Skip to content

Sidebar ​

A collapsible side bar: on desktop, collapsed narrows it to an icon strip; on mobile (narrower than mobile-breakpoint, default 768px) it automatically becomes an overlay drawer with a backdrop. Clicking outside, the close button or Esc collapses it.

Basic usage ​

Pass menu JSON via the items attribute ([{label, value, icon?}]); the default slot holds custom content.

Default sidebar

Custom content area
Right content area

Collapsing ​

Clicking the bottom「«」button toggles collapsed (a controlled attribute; fires oas-collapse). In the collapsed state only icons are shown, and menu items without an icon are hidden automatically. hide-toggle hides the button (host opt-out, e.g. for static sidebars).

Nested submenus use a flyout panel in the collapsed state: a tree parent renders as an icon item; clicking (or hovering with a short delay) opens a submenu panel anchored to the item's inline-end (mirrored to the left in RTL) with collision avoidance. Clicking a leaf fires oas-select and closes the panel; nested subtrees expand inline inside the panel; Esc closes and refocuses the parent; clicking outside closes; only one flyout is open at a time. Note (contract change): clicking a collapsed-state parent only opens the flyout and no longer fires oas-select for the parent itself (a parent is not a navigable page) — if you previously relied on the parent's oas-select in the collapsed state, listen for oas-select from the flyout's leaf items instead.

hide-toggle (no collapse button, static sidebar)

Static sidebar: no「«」collapse button at the bottom (compare with the "Collapsing" demo).

collapsed icon state

Toggle collapsed
Right content area

Mobile drawer ​

On narrow screens the sidebar automatically becomes an overlay drawer: the floating ☰ button at the top-left opens it, and the backdrop / close button / Esc collapses it. Tune the breakpoint with mobile-breakpoint (this demo uses 700px so it can be observed on a wide screen).

Mobile drawer (breakpoint 700)

Shrink the window / preview container below 700px, then click the ☰ at the top-left to open the drawer

Controlled mobile drawer ​

drawer-open is a controlled attribute: setting/removing it externally opens/closes the mobile drawer (no need to click the floating ☰). This demo raises the breakpoint to force the mobile form so it can be observed on a wide screen.

Controlled drawer-open

Open drawer (set drawer-open)Close drawer (remove drawer-open)
External buttons set drawer-open to control the drawer; after it is closed via the mask / ✕ / Esc, the attribute is removed.

Controlled highlight ​

active is a controlled attribute: setting/removing it externally migrates the currently highlighted item (no click needed).

Controlled active

Highlight DashboardHighlight SettingsClear highlight
External buttons set active to control the current highlighted item; clearing it leaves no item highlighted.

Groups ​

Menu items support an optional group field: consecutive items in the same group get a group title rendered before the group's first item (display-only, non-clickable). Group titles are hidden in the collapsed icon strip and shown in the mobile drawer.

Groups (items.group)

“Overview” and “Business” are group titles; “Profile” has no `group` field and renders flat.

Per-group independent highlight ​

active is the "current page" semantic (at most one per sidebar, aria-current="page"). For multi-group filter panels where "project / view / status" each highlight independently, use active-by-group (controlled JSON): keys are group names and values are a string for single-select within that group or an array of strings for multi-select. Group items carry aria-pressed (toggle semantics — every item in the group gets true/false) and never aria-current; both semantics can coexist on the same item.

Per-group highlight (active-by-group, with multi-select)

“Project” and “Status” are single-select; “Views” is multi-select (click toggles, no overwrite).

Project p1 · Views v1,v2 · Status doing

The host owns the state: listen for oas-select (the detail includes group) and write the next value back to active-by-group — the component holds no internal selection state (same controlled contract as active). Use an array for multi-select groups; clicks there add/remove, while single-select groups replace.

Nested submenus ​

Items support children nesting: a parent item toggles expansion on click (without firing oas-select); a parent containing the active child auto-expands. Subtrees are hidden in the collapsed icon strip. Expand/collapse runs a smooth height animation (degrades automatically under prefers-reduced-motion); accordion makes siblings mutually exclusive — expanding one parent collapses the others at the same level (great for long navs that keep only one section open).

Nesting depth: mechanically unlimited (recursive rendering; level 3 verified — per-level expansion, active-cascade auto-expansion and child-selected all work). But each level adds ~28px of indentation, so keep it to ≤3 levels in a 220px sidebar — use oas-tree for deeper trees (built for arbitrary depth: connecting lines / virtual scroll).

Three-level nesting (Business → Users → User groups)

The "Business → Users → User groups" three-level chain: with `active` pointing at the third level ("Admins"), both ancestors auto-expand with child-selected indication; indentation and guide lines stack per level.

Accordion (siblings mutually exclusive)

Click "Business", "Marketing", "Settings" in turn — only one section stays open at a time.
Each click also demos the smooth expand/collapse animation (grid height transition + chevron rotation; degrades automatically under `prefers-reduced-motion`).

Nested submenus (items.children)

“Business” auto-expands because it contains the active child (Users); clicking a parent only toggles expansion, clicking a child fires selection.

Badges and item actions ​

Items support badge count badges (colors via --oas-sidebar-badge-bg/-color variables) and actions hover action buttons (clicking fires oas-action without selecting).

Badges and item actions

Hover “Projects” to reveal action buttons; clicking fires an oas-action event.

No action yet

Leading avatars ​

Items support a leading avatar (part="avatar"): avatar is an image URL and takes precedence over initials; without an image, avatarText renders an initials circle and avatarColor (any CSS color) drives a soft tint + colored ring (the text always uses the theme text color — painting text directly on an arbitrary host color yields low contrast, so we deliberately don't), staying readable in both themes. The avatar is decorative (aria-hidden) — the accessible name still comes from the row label, so screen readers don't announce it twice; it is kept in the collapsed icon rail and the mobile drawer as well.

Leading avatars (initials / image)

The declarative child channel works the same way: <oas-sidebar-item avatar="/favicon.svg"> or avatar-text="Z" avatar-color="#e11d48"; attributes map one-to-one to the items JSON fields (avatar / avatarText / avatarColor).

Custom row content ​

renderLabel (a property function channel) can replace each item's text area: a returned Node mounts directly (two-line text, icon + description, and other rich content), a string renders as plain text, and null falls back to label. The function is not serializable, so it never lands in an attribute or SSR; the accessible name still comes from item.label, and the content hides with the label in collapsed mode. Do not put buttons/links inside the row content — the whole row is a button; use actions for interactions.

renderLabel custom row content (two lines)

Custom icons and coloring ​

Item icon defaults to the built-in @oas-ui/icons registry; app-level custom icons go through the official registerIcon(name, svg) path (exported by @oas-ui/ui) — register once, and <oas-icon>, the sidebar and any future consumers all see it (do not mutate the built-in icon data; on-demand mode using built-in names needs a one-time import '@oas-ui/icons/register'); a same-name registration overrides the built-in icon.

iconColor sets a per-item target color: when present it is fixed, taking precedence over the default disabled/active-state colors; when absent it falls back to currentColor and follows the state (active uses the primary color). Registered colored SVGs (paths with their own stroke/fill) keep their own color naturally — the outer stroke never overrides them.

Custom icons & coloring (registerIcon + iconColor)

“Workbench” is fixed to the warning color and “Repositories” to the theme primary (iconColor present → does not follow the active state); “Team feed” has no iconColor so it follows the state, and its custom SVG keeps its own red stroke. Built-in icons take iconColor too: “Built-in star” uses the registry name star + a forced red (the built-in monochrome path’s currentColor is also replaced); “Built-in heart” without one follows the state. The tooltip icon in the collapsed strip shares the same color.

Dividers and skeleton ​

A {type:"divider"} entry renders a divider line; the loading attribute shows a pulsing skeleton (the value sets the row count, default 4).

Divider + loading

Toggle loading
Click the button to toggle the skeleton.

Shortcut and keyboard navigation ​

The shortcut attribute enables Ctrl/Cmd+B collapse toggling (off by default to avoid hijacking a global key); the menu supports ↑/↓ focus movement, Home/End jumps, and Enter/Space activation.

Shortcut + keyboard navigation

Press Ctrl+B to collapse/expand; focus the menu and navigate with arrow keys.

Hover expansion (expand-on-hover) ​

The expand-on-hover attribute temporarily expands the collapsed icon strip on hover (pure visual state — does not change the controlled collapsed attribute).

expand-on-hover

Collapse to icon strip first
Hover the icon strip to temporarily expand and reveal labels; moving away collapses it again.

Variants ​

The variant attribute: sidebar (default, flush) / floating (margin + radius + shadow) / inset (margin + radius + background contrast).

variant: floating / inset

floating
inset

Right side and dual sidebars ​

side="right": the mobile drawer slides in from the right with the trigger on the right; multiple sidebars can coexist (one on each side, with independent state).

side=right + dual sidebars

“Main nav” on the left and “Auxiliary panel” (side=right) on the right — each opens/closes independently (this demo forces the mobile state for observation).

Resizable width (resizable) ​

The resizable attribute shows a drag rail on the host edge for real-time resizing (writes back to the width attribute); resize-min/resize-max clamp the range (default 160–480); the rail supports arrow keys (±8px, Home/End jumps to min/max); oas-resize fires on drag end or key adjustments. Desktop non-collapsed only (hidden when collapsed or on mobile).

resizable edge drag-resize

Drag the rail on the sidebar's right edge to resize in real time; focus the rail and use arrow keys too. Current width 220px

Resizable width (oas-splitter) ​

For resizable width, compose with oas-splitter instead of a built-in rail: place the sidebar in the left pane and drag the splitter handle to resize.

oas-splitter + sidebar

Drag the splitter handle to resize the sidebar (it fills the left pane with `width="100%"`, tracking the pane width in real time).

Custom width ​

The width attribute overrides the expanded width (defaults to the --oas-sidebar-width token); the collapsed state still narrows to an icon strip.

width attribute

180px280pxDefault token
Click the buttons above to switch the expanded width and watch the sidebar change.

Combining with oas-layout ​

oas-sidebar can be used directly as the sider of oas-layout (slot="sider").

sidebar as the sider

Header areaSide content

Main content area.

flex 1
Footer info

Declarative child channel ​

Besides the items JSON, items can be declared with <oas-sidebar-item> / <oas-sidebar-divider> child elements (the items attribute takes priority when explicitly set; otherwise children are parsed and converge to the same rendering path). The default slot text becomes the label; attributes map to the SidebarItem fields: value / icon / group / badge (all-digit strings convert to number, otherwise stay string); nesting child elements directly inside <oas-sidebar-item> recursively becomes the nested children (dividers allowed). Child additions/removals, attribute and text changes re-render automatically (MutationObserver). Boundary: actions (hover action buttons) are an object array that does not fit scalar attribute mapping — use the items JSON when you need them.

Declarative children (group / divider / nested / badge)

DashboardLive TrendsBusiness OrdersUsersInbox
“Overview” is a group title; divider, nested submenu (auto-expanded by the active child) and badge behave identically to the `items` channel.

Dynamic add/remove (MutationObserver auto-refresh)

Add an item
HomeSettings
Click the button to append a menu item; the sidebar refreshes automatically.

API ​

Methods ​

MethodDescription
openDrawer()Opens the mobile drawer programmatically (same as clicking the built-in trigger part="trigger")
closeDrawer()Closes the mobile drawer programmatically (same as mask click / close button / Esc)
  • Usage: const el = document.querySelector('oas-sidebar') as OASSidebar; el.openDrawer().
  • Equivalent to the controlled drawer-open attribute: the methods are sugar for setting/removing it (openDrawer sets, closeDrawer removes) and can be mixed; repeated calls are idempotent.
  • Mobile only (viewport narrower than mobile-breakpoint); on desktop the attribute is reclaimed by update immediately, leaving no floating layer.
  • Drawer open/close emits no event: observe the drawer-open attribute (MutationObserver) or keep host state; for a two-state toggle, call openDrawer() / closeDrawer() based on your own state.

oas-sidebar ​

Attributes ​

AttributeDescriptionTypeDefault
accordionAccordion mode (when present: expanding one parent collapses the other parents at the same level; multiple parents can be open by default)boolean—
activeValue of the currently-highlighted menu item (controlled: set/clear immediately re-renders highlight)string—
active-by-groupPer-group independent highlight (controlled JSON): group name → selected value; a string means single-select within the group, an array of strings means multi-select. Group items carry aria-pressed (toggle semantics) and never aria-current; independent from active (current page) and both can coexist on one item. Invalid JSON / non-object / non-string values are silently ignoredstring—
collapsedControlled collapse to an icon strip (present means collapsed)boolean—
drawer-openMobile drawer open state (controlled: set opens, clear closes; auto-removed when breakpoint returns to desktop)boolean—
expand-on-hoverTemporarily expand the collapsed icon strip on hover (visual only; does not change controlled collapsed)——
hide-toggleHide the bottom collapse button (a built-in toggle always visible on desktop; presence hides it — host opt-out for static sidebars)boolean—
itemsMenu items JSON [{label, value, icon?, iconColor?, group?, badge?, avatar?, avatarText?, avatarColor?, children?, actions?}] (divider entries {type:"divider"} supported; children nests submenus; avatar is an image URL taking precedence over an avatarText + avatarColor initials circle)SidebarEntry[] | string[]
loadingSkeleton loading state (shows a pulsing skeleton when present; value sets row count, default 4)string4
mobile-breakpointMobile breakpoint (px); narrower than this becomes an overlay drawer——
resizableEdge drag-resize (shows a drag rail on the host edge; desktop non-collapsed only)boolean—
resize-maxMaximum resize width (px, default 480)string480
resize-minMinimum resize width (px, default 160)string160
shortcutEnable Ctrl/Cmd+B collapse toggling (off by default to avoid hijacking a global key)boolean—
sideDrawer side: left (default) / right (mobile drawer slides from the right, trigger on the right)——
variantVariant: sidebar (default flush) / floating (radius + shadow) / inset (radius + background contrast)——
widthExpanded width; defaults to the --oas-sidebar-width tokenstring0

Property (JS property; see each row for attribute reflection) ​

PropertyDescriptionTypeDefault
renderLabelCustom row-content renderer (item) => Node | string | null (property channel; functions cannot be serialized and stay out of attributes/SSR). A returned Node mounts into the label area (for two-line or other rich content); a string renders as plain text; null/undefined falls back to item.label. The accessible name remains item.label, and the content hides with the label in collapsed mode; set null to restore default renderingSidebarLabelRenderer | null—

Events ​

EventDescription
oas-actiondetail: { value: string, action: string, label: string }; When fired: an item hover action button is clicked (does not fire oas-select)
oas-collapsedetail: { collapsed: boolean }; When fired: Desktop collapse button toggled
oas-resizedetail: { width: number }; When fired: drag-resize ends / arrow keys adjust the width
oas-selectdetail: { value: string, label: string, group?: string }; fired when a menu item is selected (the mobile drawer also closes; group lets the host write back active-by-group)

Slots ​

NameDescription
defaultMenu item content (default slot text; direct <oas-sidebar-item> children recursively become nested children)
footerCustom sidebar footer content
headerCustom sidebar header content

CSS Variables ​

CSS VariableDescriptionDefault
--oas-sidebar-avatar-colorDefault tint for leading avatars (soft background + ring); per-item avatarColor overrides it inlinevar(--oas-color-primary)
--oas-sidebar-badge-bgBadge backgroundvar(--oas-color-primary)
--oas-sidebar-badge-colorBadge text color#fff
--oas-sidebar-bgSidebar background; defaults stay anchored to base tokens (theme / dark / brand flow through automatically), host override recolors the whole barvar(--oas-color-bg-hover)
--oas-sidebar-collapsed-widthCollapsed icon-rail width64px
--oas-sidebar-item-hover-bgItem hover background (defaults to a text-primary 6% mix)color-mix(in srgb, var(--oas-color-text-primary) 6%, transparent)
--oas-sidebar-widthExpanded width (the width attribute takes precedence)220px

oas-sidebar-item ​

Attributes ​

AttributeDescriptionTypeDefault
avatarLeading avatar image URL (decorative; the name comes from label); takes precedence over avatar-text / avatar-color——
avatar-colorLeading avatar tint (any CSS color) used as a soft background + colored ring; the text always uses the theme text color, keeping contrast stable in both themes——
avatar-textLeading avatar text: renders an initials circle when there is no image (tinted by avatar-color, primary by default)——
badgeCount badge (all-digit strings convert to number, otherwise stay string; matches SidebarItem.badge's string | number)——
groupGroup name (consecutive items in the same group get a group title before the group's first item)——
iconLeading icon (@oas-ui/icons registry icon name; shown in the collapsed icon strip)——
icon-colorIcon color: when set, fixes the icon color (replaces the built-in mono path's currentColor, taking precedence over disabled/active state colors); defaults to currentColor so the color follows states——
valueSelection value (data-carrier field of the declarative child channel)——

Slots ​

NameDescription
defaultSidebar item label content (default slot text); direct child <oas-sidebar-item> (and <oas-sidebar-divider>) elements recursively become the nested children

oas-sidebar-divider ​

Slots ​

NameDescription
defaultDivider data carrier (no attributes; the host parses it as {type:"divider"})

Parts ​

root / panel / head / close / nav / body / foot / toggle (desktop collapse) / trigger (mobile trigger) / mask / item; the header and footer content are injected via slot="header", the default slot and slot="footer" respectively.

Released under the MIT OR Apache-2.0 License.