English
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
Right content area
Example
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).
Example
collapsed icon state
Right content area
Example
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
Example
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
External buttons set
drawer-open to control the drawer; after it is closed via the mask / ✕ / Esc, the attribute is removed. Example
Controlled highlight
active is a controlled attribute: setting/removing it externally migrates the currently highlighted item (no click needed).
Controlled active
External buttons set
active to control the current highlighted item; clearing it leaves no item highlighted. Example
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.
Example
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).
Example
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-treefor 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.
Example
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`).
Each click also demos the smooth expand/collapse animation (grid height transition + chevron rotation; degrades automatically under `prefers-reduced-motion`).
Example
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.
Example
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.
Example
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)
Example
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)
Example
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. Example
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
Click the button to toggle the skeleton.
Example
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.
Example
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
Hover the icon strip to temporarily expand and reveal labels; moving away collapses it again.
Example
Variants
The variant attribute: sidebar (default, flush) / floating (margin + radius + shadow) / inset (margin + radius + background contrast).
variant: floating / inset
floating
inset
Example
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).
Example
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
Example
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).
Example
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
Click the buttons above to switch the expanded width and watch the sidebar change.
Example
Combining with oas-layout
oas-sidebar can be used directly as the sider of oas-layout (slot="sider").
sidebar as the sider
Main content area.
Example
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)
“Overview” is a group title; divider, nested submenu (auto-expanded by the active child) and badge behave identically to the `items` channel.
Example
Dynamic add/remove (MutationObserver auto-refresh)
Click the button to append a menu item; the sidebar refreshes automatically.
Example
API
Methods
| Method | Description |
|---|---|
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-openattribute: the methods are sugar for setting/removing it (openDrawersets,closeDrawerremoves) and can be mixed; repeated calls are idempotent. - Mobile only (viewport narrower than
mobile-breakpoint); on desktop the attribute is reclaimed byupdateimmediately, leaving no floating layer. - Drawer open/close emits no event: observe the
drawer-openattribute (MutationObserver) or keep host state; for a two-state toggle, callopenDrawer()/closeDrawer()based on your own state.
oas-sidebar
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
accordion | Accordion mode (when present: expanding one parent collapses the other parents at the same level; multiple parents can be open by default) | boolean | — |
active | Value of the currently-highlighted menu item (controlled: set/clear immediately re-renders highlight) | string | — |
active-by-group | Per-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 ignored | string | — |
collapsed | Controlled collapse to an icon strip (present means collapsed) | boolean | — |
drawer-open | Mobile drawer open state (controlled: set opens, clear closes; auto-removed when breakpoint returns to desktop) | boolean | — |
expand-on-hover | Temporarily expand the collapsed icon strip on hover (visual only; does not change controlled collapsed) | — | — |
hide-toggle | Hide the bottom collapse button (a built-in toggle always visible on desktop; presence hides it — host opt-out for static sidebars) | boolean | — |
items | Menu 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 | [] |
loading | Skeleton loading state (shows a pulsing skeleton when present; value sets row count, default 4) | string | 4 |
mobile-breakpoint | Mobile breakpoint (px); narrower than this becomes an overlay drawer | — | — |
resizable | Edge drag-resize (shows a drag rail on the host edge; desktop non-collapsed only) | boolean | — |
resize-max | Maximum resize width (px, default 480) | string | 480 |
resize-min | Minimum resize width (px, default 160) | string | 160 |
shortcut | Enable Ctrl/Cmd+B collapse toggling (off by default to avoid hijacking a global key) | boolean | — |
side | Drawer side: left (default) / right (mobile drawer slides from the right, trigger on the right) | — | — |
variant | Variant: sidebar (default flush) / floating (radius + shadow) / inset (radius + background contrast) | — | — |
width | Expanded width; defaults to the --oas-sidebar-width token | string | 0 |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
renderLabel | Custom 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 rendering | SidebarLabelRenderer | null | — |
Events
| Event | Description |
|---|---|
oas-action | detail: { value: string, action: string, label: string }; When fired: an item hover action button is clicked (does not fire oas-select) |
oas-collapse | detail: { collapsed: boolean }; When fired: Desktop collapse button toggled |
oas-resize | detail: { width: number }; When fired: drag-resize ends / arrow keys adjust the width |
oas-select | detail: { 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
| Name | Description |
|---|---|
| default | Menu item content (default slot text; direct <oas-sidebar-item> children recursively become nested children) |
footer | Custom sidebar footer content |
header | Custom sidebar header content |
CSS Variables
| CSS Variable | Description | Default |
|---|---|---|
--oas-sidebar-avatar-color | Default tint for leading avatars (soft background + ring); per-item avatarColor overrides it inline | var(--oas-color-primary) |
--oas-sidebar-badge-bg | Badge background | var(--oas-color-primary) |
--oas-sidebar-badge-color | Badge text color | #fff |
--oas-sidebar-bg | Sidebar background; defaults stay anchored to base tokens (theme / dark / brand flow through automatically), host override recolors the whole bar | var(--oas-color-bg-hover) |
--oas-sidebar-collapsed-width | Collapsed icon-rail width | 64px |
--oas-sidebar-item-hover-bg | Item hover background (defaults to a text-primary 6% mix) | color-mix(in srgb, var(--oas-color-text-primary) 6%, transparent) |
--oas-sidebar-width | Expanded width (the width attribute takes precedence) | 220px |
oas-sidebar-item
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
avatar | Leading avatar image URL (decorative; the name comes from label); takes precedence over avatar-text / avatar-color | — | — |
avatar-color | Leading 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-text | Leading avatar text: renders an initials circle when there is no image (tinted by avatar-color, primary by default) | — | — |
badge | Count badge (all-digit strings convert to number, otherwise stay string; matches SidebarItem.badge's string | number) | — | — |
group | Group name (consecutive items in the same group get a group title before the group's first item) | — | — |
icon | Leading icon (@oas-ui/icons registry icon name; shown in the collapsed icon strip) | — | — |
icon-color | Icon 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 | — | — |
value | Selection value (data-carrier field of the declarative child channel) | — | — |
Slots
| Name | Description |
|---|---|
| default | Sidebar 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
| Name | Description |
|---|---|
| default | Divider 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.