Skip to content

Sidebar

NavigationSince 0.3.0
import { Sidebar } from '@absoluteui/components/sidebar';
import '@absoluteui/components/sidebar/style.css';

Using Angular? See Sidebar for Angular.

Application side navigation. Sidebar renders a labelled <nav> with a list of links (buttons for items without an href) and nested groups that expand as disclosures (WAI-ARIA APG Disclosure Navigation). The active item carries aria-current="page". The sidebar can collapse to an icon rail whose labels appear as tooltips, and below a viewport breakpoint it moves into a modal Drawer that the application opens from its own menu button. Activation emits a cancelable aui-navigate, so client-side routers can take over. Collapsed, expanded-group, and mode changes are reported through events; nothing is persisted.

  • Primary or secondary navigation of an application with more destinations than fit in a top bar.
  • Hierarchical navigation with one or two levels of groups.
  • Layouts that need a compact icon rail on desktop and a drawer on small screens.
  • A handful of top-level destinations: use a top navigation bar or tabs.
  • Actions or commands (not destinations): use Menu or buttons.
  • Showing the current page’s position in a hierarchy: use Breadcrumb.
  • Deep trees (more than three levels) or file-system-like hierarchies: use a tree view.

Application navigation with a nested group

Section titled “Application navigation with a nested group”
import { Sidebar, type SidebarItem } from '@absoluteui/components/sidebar';
const host = document.createElement('aside');
document.body.append(host);
const items: SidebarItem[] = [
{ id: 'dashboard', label: 'Dashboard', href: '/', icon: 'menu' },
{
type: 'group',
id: 'reports',
label: 'Reports',
icon: 'calendar',
items: [
{ id: 'sales', label: 'Sales', href: '/reports/sales' },
{ id: 'usage', label: 'Usage', href: '/reports/usage' },
],
},
{ id: 'search', label: 'Search', href: '/search', icon: 'search' },
{ id: 'billing', label: 'Billing', href: '/billing', icon: 'info', disabled: true },
];
Sidebar.create(host, { label: 'Main', items, value: 'sales' });

Client-side routing and a menu button for drawer mode

Section titled “Client-side routing and a menu button for drawer mode”
import { Sidebar, type SidebarModeChangeDetail, type SidebarNavigateDetail } from '@absoluteui/components/sidebar';
const host = document.querySelector<HTMLElement>('#app-sidebar');
const menuButton = document.querySelector<HTMLButtonElement>('#app-menu');
if (host && menuButton) {
const sidebar = Sidebar.create(host, {
label: 'Main',
breakpoint: 1024,
items: [
{ id: 'inbox', label: 'Inbox', href: '/inbox' },
{ id: 'archive', label: 'Archive', href: '/archive' },
{ id: 'settings', label: 'Settings', href: '/settings' },
],
value: 'inbox',
});
// The application owns the menu button; it is needed only in drawer mode.
menuButton.hidden = sidebar.mode === 'inline';
menuButton.addEventListener('click', () => {
sidebar.toggle();
});
host.addEventListener('aui-mode-change', (event) => {
menuButton.hidden = (event as CustomEvent<SidebarModeChangeDetail>).detail.mode === 'inline';
});
// SPA routing: cancel native navigation, route, then mark the item active and close the drawer.
host.addEventListener('aui-navigate', (event) => {
const { id, item } = (event as CustomEvent<SidebarNavigateDetail>).detail;
if (item.href === undefined) return;
event.preventDefault();
history.pushState(null, '', item.href);
sidebar.select(id);
sidebar.close();
});
}

Icon rail with a remembered collapsed state

Section titled “Icon rail with a remembered collapsed state”
import { Sidebar, type SidebarChangeDetail, type SidebarCollapsedChangeDetail } from '@absoluteui/components/sidebar';
const host = document.querySelector<HTMLElement>('#workspace-nav');
if (host) {
// The sidebar persists nothing; the application may remember the rail state itself.
let stored: string | null = null;
try {
stored = localStorage.getItem('workspace-nav-collapsed');
} catch {
stored = null;
}
Sidebar.create(host, {
label: 'Workspace',
collapsed: stored === 'true',
items: [
{ id: 'calendar', label: 'Calendar', icon: 'calendar' },
{ id: 'tasks', label: 'Tasks', icon: 'check' },
{ id: 'activity', label: 'Activity', icon: 'clock' },
],
value: 'calendar',
});
host.addEventListener('aui-collapsed-change', (event) => {
const { collapsed } = (event as CustomEvent<SidebarCollapsedChangeDetail>).detail;
try {
localStorage.setItem('workspace-nav-collapsed', String(collapsed));
} catch {
// Storage unavailable (private mode, blocked): keep the in-memory state only.
}
});
// Items without href are buttons: switch views in place.
host.addEventListener('aui-change', (event) => {
document.body.dataset.view = (event as CustomEvent<SidebarChangeDetail>).detail.value;
});
}
import { Sidebar, type SidebarModeChangeDetail, type SidebarNavigateDetail } from '@absoluteui/components/sidebar';
import '@absoluteui/components/sidebar/style.css';
import '@absoluteui/components/drawer/style.css';
import '@absoluteui/components/button/style.css';
import '@absoluteui/components/tooltip/style.css';
const host = document.querySelector<HTMLElement>('#app-sidebar');
const menuButton = document.querySelector<HTMLButtonElement>('#app-menu');
if (host && menuButton) {
const sidebar = Sidebar.create(host, {
label: 'Main',
value: 'inbox',
items: [
{ id: 'inbox', label: 'Inbox', href: '/inbox' },
{ id: 'archive', label: 'Archive', href: '/archive' },
],
});
menuButton.hidden = sidebar.mode === 'inline';
menuButton.addEventListener('click', () => {
sidebar.toggle();
});
host.addEventListener('aui-mode-change', (event) => {
menuButton.hidden = (event as CustomEvent<SidebarModeChangeDetail>).detail.mode === 'inline';
});
host.addEventListener('aui-navigate', (event) => {
const { id, item } = (event as CustomEvent<SidebarNavigateDetail>).detail;
if (item.href === undefined) return;
event.preventDefault();
history.pushState(null, '', item.href);
sidebar.select(id);
sidebar.close();
});
}
import { Sidebar } from '@absoluteui/components/sidebar';
const host = document.createElement('aside');
document.body.append(host);
Sidebar.create(host, {
label: 'Main',
value: 'sales',
items: [
{ id: 'dashboard', label: 'Dashboard', href: '/', icon: 'menu' },
{ type: 'group', id: 'reports', label: 'Reports', items: [{ id: 'sales', label: 'Sales', href: '/reports/sales' }] },
],
});
<aside class="aui-sidebar"> (consumer host)
<nav class="aui-sidebar__nav" id="aui-sidebar-…" aria-label="Main">
<ul class="aui-sidebar__list">
<li class="aui-sidebar__entry">
<a class="aui-sidebar__item" href="/" data-id="home" aria-current="page">
<span class="aui-sidebar__icon" aria-hidden="true"><svg class="aui-icon">...</svg></span>
<span class="aui-sidebar__label">Home</span>
</a>
</li>
<li class="aui-sidebar__entry">
<button type="button" class="aui-sidebar__item aui-sidebar__group" data-id="reports" aria-controls="aui-sidebar-group-…" aria-expanded="true">
<span class="aui-sidebar__icon aui-sidebar__icon--initial" aria-hidden="true">R</span> (no icon: first letter, rail only)
<span class="aui-sidebar__label">Reports</span>
<svg class="aui-icon aui-sidebar__chevron" aria-hidden="true">
</button>
<ul class="aui-sidebar__list aui-sidebar__list--nested" id="aui-sidebar-group-…">...</ul>
</li>
<li class="aui-sidebar__entry"><button type="button" class="aui-sidebar__item" data-id="settings">...</button></li> (no href)
<li class="aui-sidebar__entry"><a class="aui-sidebar__item" role="link" aria-disabled="true" data-id="billing">...</a></li> (disabled)
</ul>
</nav>
<div class="aui-sidebar__footer"> (hidden when collapsible: false)
<button type="button" class="aui-sidebar__toggle" aria-controls="aui-sidebar-…" aria-label="Collapse sidebar"><svg class="aui-icon">...</svg></button>
</div>
</aside>

In drawer mode the <nav> moves into the body of a modal <dialog class="aui-drawer"> appended to document.body, and the host gets aui-sidebar--drawer (display: none). In the rail, each item (including disabled ones), group button, and the toggle get a role="tooltip" element (appended to document.body, or to the enclosing <dialog>).

Sidebar.create(host: HTMLElement, options: SidebarOptions): Sidebar

Global configuration key: sidebar (see Configuration).

Option Type Default Description
label string Accessible name of the <nav> (aria-label) and the drawer title, e.g. “Main”. Must not be empty. Required.
items readonly SidebarItem[] (SidebarLink { type?: 'item'; id; label; href?; icon?; disabled? } | SidebarGroup { type: 'group'; id; label; icon?; items }) Navigation tree (at least one item; groups need at least one child). Ids are unique across the whole tree; labels are non-empty. Links without href render as buttons. Required.
value string Active (current page) item id; must be a non-group item. Default: no active item.
collapsed boolean false Icon rail: labels are visually hidden and shown as tooltips. Inline mode only; forced to false when collapsible is false.
expanded readonly string[] "the groups containing the active item" Expanded group ids. Every id must be a group id.
collapsible boolean true Render the collapse (rail) toggle in the footer. Globally configurable.
breakpoint number 768 Integer 0..4096: viewport width in CSS px below which the sidebar becomes a modal drawer. 0 never switches. Globally configurable.
direction 'ltr' | 'rtl' "ltr" ‘rtl’ sets dir=“rtl” on the <nav> and footer; tooltips and the drawer also follow an inherited rtl direction. Globally configurable. Inherited from common configuration.
icons Partial<{ group: IconRef; collapse: IconRef; expand: IconRef }> Icon slot overrides.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
nav HTMLElement The <nav> landmark. It lives in the host inline and in the drawer body in drawer mode.
value string | null Active item id, or null.
collapsed boolean Whether the icon rail is requested (applied inline only).
expanded string[] Expanded group ids, in tree order.
mode 'inline' | 'drawer' ‘drawer’ below the breakpoint, otherwise ‘inline’.
isOpen boolean Whether the drawer is open (always false inline).
options Readonly<SidebarOptions> Current instance options.
Method Description
open(): void Open the drawer; focus moves into it. No-op in inline mode.
close(): void Close the drawer; focus returns to the element focused before open(). No-op when closed or inline.
toggle(): void close() when open, otherwise open().
select(id: string): void Mark a non-group item active and expand its groups. No events; does not close the drawer. Throws SIDEBAR_OPTIONS for an unknown or group id.
update(options: Partial<SidebarOptions>): void Merge, validate, and apply options. Focus stays on the item with the same id. An active id missing from new items becomes null. No events.
destroy(): void Remove rendering, host classes, tooltips, the drawer, the media query listener, listeners, and the configuration subscription.
Event Detail Bubbles Cancelable Description
aui-navigate { id: string; item: SidebarLink } Yes Yes The user activated an enabled item (unmodified primary click, Enter, or Space on a button). preventDefault stops link navigation, the active change, and the drawer close.
aui-change { value: string; previous: string | null } Yes No The active item changed after an uncanceled aui-navigate. Not emitted when the active item is activated again.
aui-expanded-change { id: string; expanded: boolean; value: string[] } Yes No The user toggled a group. value lists all expanded group ids in tree order.
aui-collapsed-change { collapsed: boolean } Yes No The user activated the collapse toggle.
aui-mode-change { mode: 'inline' | 'drawer' } Yes No The viewport crossed the breakpoint after creation. The initial mode is not reported.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-sidebar Host. Sets –aui-sidebar-width (default 16rem) and –aui-sidebar-rail-width.
aui-sidebar--collapsed Icon rail (inline mode only).
aui-sidebar--drawer Drawer mode; the host is display:none.
aui-sidebar__nav The <nav> landmark.
aui-sidebar__list | __list--nested Top-level and group <ul>.
aui-sidebar__entry An <li>.
aui-sidebar__item Link, button, disabled link, or group button; data-id is the item id.
aui-sidebar__group Group disclosure button (also has aui-sidebar__item).
aui-sidebar__item--contains-active Collapsed group that contains the active item.
aui-sidebar__icon | __icon--initial | __label | __chevron Item parts; __icon–initial is the first-letter fallback shown only in the rail.
aui-sidebar__footer | __toggle Footer and the collapse toggle button.
Slot Default icon
group chevron-down
collapse chevrons-left
expand chevrons-right

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-semibold | --aui-icon-size-md | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4

interface SidebarConfig { collapsible: boolean; breakpoint: number }
interface SidebarLink { type?: 'item'; id: string; label: string; href?: string; icon?: IconRef; disabled?: boolean }
interface SidebarGroup { type: 'group'; id: string; label: string; icon?: IconRef; items: readonly SidebarItem[] }
type SidebarItem = SidebarLink | SidebarGroup;
type SidebarMode = 'inline' | 'drawer';
interface SidebarIcons { group: IconRef; collapse: IconRef; expand: IconRef }
interface SidebarOptions extends Partial<SidebarConfig> {
label: string; items: readonly SidebarItem[]; value?: string; collapsed?: boolean;
expanded?: readonly string[]; direction?: Direction; icons?: Partial<SidebarIcons>;
}
interface SidebarNavigateDetail { id: string; item: SidebarLink }
interface SidebarChangeDetail { value: string; previous: string | null }
interface SidebarExpandedChangeDetail { id: string; expanded: boolean; value: string[] }
interface SidebarCollapsedChangeDetail { collapsed: boolean }
interface SidebarModeChangeDetail { mode: SidebarMode }

Pattern: WAI-ARIA APG Disclosure Navigation: a navigation landmark with lists of links, groups as disclosure buttons (aria-expanded + aria-controls), and aria-current=“page” on the active item. Drawer mode uses the Drawer (modal <dialog>) pattern; the rail uses Tooltip as the accessible name.

Key Context Action
Tab / Shift+Tab Any Moves between enabled items, group buttons, and the collapse toggle (native tab order). Disabled items and items inside collapsed groups are skipped.
Enter Link item focused Emits cancelable aui-navigate; unless prevented, the item becomes active (aui-change), the drawer closes in drawer mode, and the link navigates natively.
Enter / Space Button item focused Same as a link, without native navigation.
Enter / Space Group button focused Expands or collapses the group in place (focus stays) and emits aui-expanded-change.
Enter / Space Collapse toggle focused Switches between the full sidebar and the icon rail and emits aui-collapsed-change.
Escape Drawer open Closes the drawer and restores focus (Drawer behavior; follows the drawer closeOnEscape configuration).
Escape Rail tooltip visible Hides the tooltip (Tooltip behavior).
Element Role
<nav> navigation
<ul> / <li> list / listitem
item with href link (native <a href>)
item without href button (native <button type=“button”>)
disabled item link (<a role=“link”> without href)
group button (native <button type=“button”>)
rail tooltip tooltip
drawer (drawer mode) dialog (native modal <dialog>)
Attribute Element Value
aria-label <nav> label option
aria-current active item page
aria-expanded group button true | false
aria-controls group button id of the nested <ul>
hidden nested <ul> Present while the group is collapsed
aria-disabled disabled item true (the element has no href and is not focusable)
aria-hidden item icon / initial, chevron true
aria-label collapse toggle Localized “Collapse sidebar” or “Expand sidebar”
aria-controls collapse toggle id of the <nav>
aria-labelledby each enabled item, group, and the toggle (icon rail only) id of its tooltip, whose text is the label
aria-labelledby drawer <dialog> (drawer mode) id of the drawer title (the label option)
dir <nav> and footer rtl when direction is ‘rtl’; absent otherwise

Every enabled item and group button is a Tab stop. Toggling a group, activating an item, and update() keep focus on the same item id. In drawer mode, open() moves focus into the modal drawer and close() returns it to the element focused before opening (usually the application’s menu button).

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard A
2.1.2 No Keyboard Trap A
2.4.1 Bypass Blocks A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • The active item is conveyed by aria-current=“page”, a selected background, and semibold weight, not color alone.
  • In the icon rail, labels stay in the DOM (visually hidden) and each control is also labelled by its tooltip, so names do not change when the sidebar collapses.
  • Collapsed groups hide their lists with the hidden attribute, removing them from the accessibility tree and the tab order.
  • Drawer mode inherits Drawer’s modal behavior: inert background, focus containment, and focus restoration.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
  • Disabled items are not focusable and have no rail tooltip; they are reachable only in a screen reader’s browse mode.
  • No arrow-key navigation (optional in the Disclosure Navigation pattern); long sidebars take many Tab presses.

Sidebar: inline, collapsed (icon rail), drawer closed, drawer open. Items: default, hover, focus-visible, active (aria-current="page"), disabled. Groups: expanded, collapsed, collapsed containing the active item.

From Trigger To
item A active enabled item B activated aui-navigate; unless prevented: B active, its groups expand, aui-change, drawer closes
any aui-navigate prevented unchanged; links do not navigate; drawer stays open
group collapsed group button activated expanded, aui-expanded-change (and back)
full toggle activated rail, aui-collapsed-change (and back)
inline viewport narrower than breakpoint drawer (closed), aui-mode-change
drawer viewport at or above breakpoint inline, drawer closed, aui-mode-change
drawer closed open() / toggle() drawer open, focus inside
drawer open close(), Escape, backdrop, close button, uncanceled navigation drawer closed, focus restored
any collapsible becomes false full width, toggle hidden, no event
  • The active item is conveyed by aria-current, a selected background, and semibold weight, not color alone.
  • Accessible names do not change between full and rail views.
  • Toggling a group, activating an item, and update() keep focus on the same item id.
  • Drawer mode inherits Drawer’s modal behavior: inert background, focus containment, focus restoration to the menu button.
  • The application’s menu button that opens the drawer needs its own accessible name.
  • In the rail, disabled items also get a tooltip (their only visible name).
  • Manual screen-reader verification is pending (see Known limitations).

Inline width is --aui-sidebar-width, or --aui-sidebar-rail-width in the rail, animated with duration-fast (none under reduced motion or data-aui-animation='none'). The host is max-inline-size: 100%; the <nav> scrolls when items overflow. Below breakpoint viewport px (matchMedia('(max-width: breakpoint − 0.02px)')) the <nav> moves into a modal start-edge Drawer (size sm), the host is removed from layout, and collapsed is ignored. At or above the breakpoint the drawer closes and the <nav> returns to the host. breakpoint: 0 keeps the sidebar inline at every width.

Logical properties throughout: the border sits on the inline-end edge; nested lists indent from the inline start. direction: 'rtl' sets dir="rtl" on the <nav> and footer; with 'ltr' no dir is set, so an ancestor dir="rtl" applies. The toggle chevrons are mirrored under :dir(rtl). Rail tooltips open to the left in RTL and to the right in LTR. The drawer opens from the right edge in RTL. The RTL state for tooltips and the drawer is resolved on render; changing an ancestor dir later takes effect on the next render (update() or a configuration change).

Role tokens only. Hover: color-hover. Active: color-selected background, color-selected-text, semibold. Disabled: color-text-disabled. Forced-colors mode: Highlight/HighlightText active item, GrayText disabled items, CanvasText border.

configureAbsoluteUI({ components: { sidebar: { collapsible: false, breakpoint: 1024 } } });

Changes apply to existing instances immediately, including the breakpoint listener. The drawer’s own options (closeOnEscape, closeOnBackdrop, showCloseButton) follow the drawer global configuration; placement (start) and size (sm) are fixed by the sidebar.

Instance options override global and theme defaults. All options are dynamic through update().

Slot Default Used for
icons.group chevron-down Group indicator (rotated 180 deg while expanded).
icons.collapse chevrons-left Toggle while full (mirrored in RTL).
icons.expand chevrons-right Toggle while collapsed (mirrored in RTL).

Each item’s icon is an IconRef. Items without icon show the first character of their label in the rail only.

Labels use textContent. Each href is parsed with the URL standard against document.baseURI; only http:/https: and scheme-less URLs are accepted. javascript:, data:, and other schemes, including obfuscations such as java\tscript:, throw SIDEBAR_OPTIONS; the error names the property path but never echoes the value. CSP-safe.

No persistence, network access, or telemetry. Active item, expanded groups, collapsed state, and mode live in memory. To remember them, store the event details (aui-collapsed-change, aui-expanded-change) and pass them back as collapsed / expanded.

One click listener on the <nav>, one on the toggle, one configuration subscription, and one media query listener (none when breakpoint is 0) per instance. Active, expanded, and collapsed changes update attributes in place; update() and configuration changes rebuild the list. The rail creates one Tooltip per control. Budget: Sidebar.create with 200 items in nested groups under 50 ms in headless Chromium.

Up to a few hundred items across up to three levels. Tested with 4 top-level items and a group nested two levels deep.

  • Without value, no item is active and all groups start collapsed (unless expanded is given).
  • Groups cannot be active; value and select() accept non-group items only.
  • An item is a group only when type: 'group'; an item with items but without type: 'group' throws SIDEBAR_OPTIONS.
  • Activating the already active item emits aui-navigate (and closes the drawer) but no aui-change.
  • collapsible: false forces collapsed to false without an event; it stays false if collapsible becomes true again.
  • collapsed: true in drawer mode is kept but not applied until the sidebar returns inline.
  • Groups remain operable in the rail; their children show below them without indentation.
  • update({ items }) without value keeps the active id when it still exists, otherwise sets it to null.
  • select() does not close the drawer.
  • Opening the drawer inline (open()) is a no-op; no dialog is created until the first switch to drawer mode.
Code Category Cause
SIDEBAR_OPTIONS configuration Empty label; empty items or group items; an item that is not an object; an empty or duplicate id (across the tree); an empty item label; type other than 'item'/'group'; an unsafe or unparsable href; value (or select(id)) not a non-group item; non-boolean collapsed; expanded not an array of group ids. property is a path such as sidebar.items[1].items[0].href.
CFG_INVALID_VALUE configuration Invalid collapsible, breakpoint (not an integer 0-4096), or direction (instance or global configuration).

A failed construction leaves the host untouched: no children, classes, listeners, subscriptions, drawer, or media query listener. A failed update() keeps the previous options and rendering.

  • No arrow-key navigation; every enabled item is a Tab stop.
  • The breakpoint follows the viewport, not the container.
  • The application must provide the menu button that opens the drawer.
  • Nothing is persisted.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • HTML or custom nodes in labels.
  • Hrefs with schemes other than http and https.
  • Groups that are also links.
  • Badges, counters, or secondary actions inside items.
  • Drag-to-resize.