Sidebar
Composable app-shell navigation.
Usage
Custom rather than a Bits wrapper — Bits ships no sidebar, and there is no hard behaviour to buy here. What a sidebar needs is a landmark, a labelled list, a shared collapsed state and a toggle that announces itself, all of which the platform gives directly.
Toggle it — the labels hide, the items stay reachable, and "Reports" is a non-link.
Why Provider is separate
Provider owns the state; Root is the <aside>. They are separate parts for one concrete reason: Svelte
context reaches descendants, not siblings.
Put the state on Root and a Trigger sitting in
a top bar next to the sidebar never sees it — and with collapsible="offcanvas" the Trigger has to be outside, or
collapsing hides the only way back. A test caught exactly that: the Trigger got a null aria-controls and clicking it did nothing.
The Provider imposes no layout unless you pass shell. By default it is display: contents, so
it is invisible to layout and the children sit where they would without it — which matters
when the sidebar is a child of a grid your app already defined. shell opts
into the flex row.
Collapse modes
icon— narrows to a rail. Labels hide, items stay reachable.offcanvas— slides out entirely, and its contents become unfocusable so the cursor cannot vanish into a hidden panel.none— not collapsible; the Trigger becomes pointless.
collapsed is bindable on the Provider. Persist it — a sidebar
that forgets its state on every navigation is worse than one that never collapsed.
Size it with --sve-sidebar-width and --sve-sidebar-width-icon rather than overriding rules.
No JS media query
The collapsed presentation is one CSS class. This component does not watch the viewport.
A library that swaps the markup for a drawer below some breakpoint hardcodes that breakpoint
inside itself, cannot know it during server rendering, and flashes the wrong layout on
hydration. collapsible="offcanvas" plus your breakpoint gets the same result,
and your app decides when it happens.
Accessibility
Rootis a named<aside>— an app shell usually has more than one complementary region.- Point each
Group'saria-labelledbyat itsGroupLabelid. A visual heading assistive technology cannot connect to its items is decoration, not structure. - On an icon rail the
GroupLabelis hidden visually, not removed — deleting it would strip the group of its name for someone who has plenty of room for it. - Give icon-only
Items alabel. It becomes the accessible name when collapsed; without it the rail is a column of unnamed links. Itemsetsaria-current="page"whenactive— not just a highlight, which tells sighted users and nobody else.disabledrenders a<span>, not a link. A link that goes nowhere takes a tab stop and lies about what will happen.- The Trigger's label stays the same in both states.
aria-expandedalready carries the state; a label that flips to "Close" says it twice and contradicts itself mid-announcement.
Props
Sidebar.Provider — collapsed is bindable.
| Prop | Type | Default |
|---|---|---|
collapsed Whether the sidebar is collapsed. Bindable, so the app can persist it — a sidebar that forgets its state on every navigation is worse than one that never collapsed. | boolean | false |
collapsible How collapsing presents itself. `icon` — narrows to a rail; labels hide, items stay reachable. `offcanvas` — slides out of view entirely. `none` — not collapsible; the Trigger becomes pointless. | Collapsible | 'icon' |
side Which edge the sidebar is anchored to. | Side | 'left' |
shell Lay the children out as an app shell — a flex row with the sidebar beside the main content (reversed when `side="right"`). OFF by default, and deliberately so: this component's job is state, and a provider that silently imposes a layout fights whatever the app already has. Without it the wrapper is `display: contents`, so it does not participate in layout at all and the children behave as if it were not there — which is what you want when the sidebar sits inside a grid the app already defined. Found by trying to rebuild this library's own docs navigation on top of this component: the forced flex row put the mobile toggle beside the panel instead of above it. | boolean | false |
sidebarId Id given to the sidebar element, so a Trigger anywhere inside this provider can point `aria-controls` at it. | string | 'sve-sidebar' |
class Extra classes merged onto the shell. | string | — |
children | Snippet | — |
Sidebar.Root
| Prop | Type | Default |
|---|---|---|
label Accessible name for the landmark. An app shell usually has more than one complementary region, so this is how a screen reader user tells them apart. | string | 'Sidebar' |
class Extra classes merged onto the root. | string | — |
children | Snippet | — |
Sidebar.Item — plus the native anchor attributes.
| Prop | Type | Default |
|---|---|---|
active Marks the current page. It sets `aria-current="page"`, not just a highlight — styling it without that tells sighted users and nobody else. | boolean | false |
label Accessible name used when the label is hidden on an icon rail. Required in practice for icon-only items: without it a collapsed sidebar becomes a column of unnamed links. | string | — |
disabled A visible entry that cannot be navigated to — a feature not shipped yet, a section the user lacks access to. It renders a `<span>` rather than an `<a>`, because a link that goes nowhere is worse than not being a link: it takes a tab stop and lies about what will happen. Found by rebuilding this library's own docs sidebar on top of this component, which needed exactly that for its "soon" entries. | boolean | false |
class Extra classes merged onto the item. | string | — |
children | Snippet | — |
Sidebar.Trigger
| Prop | Type | Default |
|---|---|---|
label Accessible name. It stays the SAME in both states — `aria-expanded` already carries whether the sidebar is open, so a label that flips between "Open" and "Close" says it twice and contradicts itself mid-announcement. | string | 'Toggle sidebar' |
class Extra classes merged onto the button. | string | — |
children | Snippet | — |
Header, Content, Footer, Group, GroupLabel and Menu each take class plus their native attributes.