Sidebar
Vertical navigation panel for app layouts. No fixed positioning, width, or background by default; the layout decides where it sits.
Basic
One content slot, one group, one list. The current page is marked with aria-current="page" on the matching button.
With header and footer
Use .sidebar__header for the brand mark; .sidebar__brand lines up an icon and a wordmark. .sidebar__footer pins to the bottom via margin-block-start: auto.
Sizes
Three sizes: .sidebar--sm, the default, and .sidebar--lg. The modifier retunes button height, padding, and group gap. Outer panel padding stays the same so gutters read identically across sizes.
Groups and group action
.sidebar__group-title labels each list. An optional .sidebar__group-action sits to the right of the title, useful for an add or filter button. Its contents collapse to a uniform square regardless of the button modifiers passed in. Pick a tone via .button--ghost.button--neutral and the slot handles the chrome.
Active state
Two hooks. aria-current="page" marks the current page on a navigation link; data-state="active" covers non-link rows (a button toggling a panel, say). Both paint the highlight chip. The difference is semantic; it looks the same.
Item actions
Place a .sidebar__item-action after the button to drop a badge or quiet button into the right edge of the row. The button keeps its full click area; the action overlays only its own pixels. Add the --reveal modifier to show the action only on row hover or keyboard focus, for dense lists where always-visible actions feel noisy.
Nested submenu
Wrap a child .sidebar__list in a .sidebar__submenu inside the same .sidebar__item. The item carries data-state="open" or data-state="closed"; an empty <span class="sidebar__caret"> inside the trigger renders a chevron that rotates when the parent reports aria-expanded="true". The first item starts open, the second closed. Toggling is wired by @stisla/vanilla via data-stisla-sidebar-submenu-toggle — click a parent to expand it.
Link parent with submenu
When the parent also maps to a real page, split the two: keep the label as an <a> and move the disclosure into a caret button in the .sidebar__item-action slot. The label navigates, the caret opens the tree, and each is the right element for assistive tech.
As a panel
The sidebar background is transparent by default. To frame it as a standalone panel, set --sidebar-bg to a surface token, add a border, and round the corners.
Recolor
Every visible part wires to a --sidebar-* variable. Override on the panel itself, on a scoped class, or on :root. The example tints the sidebar with the primary brand color, paired with --color-primary-foreground for the chip text.
Rail / mini mode
Add data-collapsed to the panel to shrink it to icons only. Each button becomes a square with its icon centered; labels, submenus, and chevrons fade out coordinated with whatever width transition the layout runs. Wrap label text in a <span> so it can be hidden cleanly. The button size follows .sidebar--sm / .sidebar--lg; the collapsed panel hugs the icon column on its own.
A data-stisla-sidebar-toggle="collapse" button flips the state; the panel animates between its full and rail width over --sidebar-transition-duration. Click Collapse below.
Statically collapsed at each size, for reference:
Collapse toggle
Add data-stisla-sidebar-toggle="collapse" to any element to flip the panel between expanded and rail. @stisla/vanilla toggles [data-collapsed] on the closest [data-stisla-sidebar], or, when the trigger lives outside the sidebar, on the panel referenced by aria-controls. The trigger's aria-expanded stays in sync, and a stisla:sidebar:collapse-change event bubbles up so you can react to the state in your own code.
The width animates pure-CSS on its own (no inline width changes, no script). The panel is 16rem expanded and the collapsed rail derives from the icon cell, so both ends are real lengths the transition can interpolate over --sidebar-transition-duration. Set --sidebar-width / --sidebar-width-collapsed only to override those defaults.
Any open submenus close in sync with the width transition: the framework calls closeAllSubmenus() as part of the collapse so each height animation runs alongside the rail shrink. Expanding back doesn't reopen them; the user clicks back into whichever menu they want. To drive the toggle from outside the sidebar (a topbar button, a keyboard shortcut), point at it with aria-controls set to the sidebar's id.
Or call the API directly with Stisla.get(sidebarEl).toggleCollapsed(), .collapse(), .expand(), or .isCollapsed(). Persist the state to localStorage by listening for stisla:sidebar:collapse-change.
Customization
These variables retune .sidebar without touching component CSS. Override on the panel, a parent scope, or :root.
Shell
| Variable | Use |
|---|---|
--sidebar-bg | Panel background |
--sidebar-color | Panel foreground (brand and buttons inherit) |
--sidebar-padding-block | Top and bottom padding on header / footer |
--sidebar-padding-inline | Panel outer gutter; feeds the rail-width recipe |
--sidebar-gap | Space between header, content, footer |
--sidebar-width | Expanded panel width. Opt-in; set it to let the sidebar own its width and animate the collapse. Loses to inline width or a parent that sizes the panel. |
--sidebar-width-collapsed | Rail width applied under [data-collapsed]. Pair with --sidebar-width and the flip animates via --sidebar-transition-duration. |
Brand
| Variable | Use |
|---|---|
--sidebar-brand-color | Brand text color |
--sidebar-brand-icon-size | Brand icon dimensions; load-bearing for rail re-center math |
--sidebar-brand-gap | Space between brand icon and wordmark |
Button (item)
| Variable | Use |
|---|---|
--sidebar-button-height | Hard height |
--sidebar-button-padding-inline | Horizontal inset. Load-bearing for rail symmetry: padding + icon-size + padding equals button-height so the icon centers at rail width with no justify-content override. |
--sidebar-button-padding-block | Vertical padding; defaults to 0 since the row height owns the rhythm |
--sidebar-button-radius | Corner radius |
--sidebar-button-gap | Space between icon and label |
--sidebar-button-font-weight | Label weight |
--sidebar-button-color | Rest text color |
--sidebar-button-bg-hover | Hover chip fill |
--sidebar-button-color-hover | Hover text color |
--sidebar-button-bg-active | Active chip fill (aria-current="page" or data-state="active") |
--sidebar-button-color-active | Active text color |
--sidebar-button-icon-size | Icon dimensions; load-bearing for the rail recipe and the chevron |
--sidebar-button-icon-color | Icon color at rest and hover. Active rows flip the icon to --sidebar-button-color-active so the row reads unified. |
Item action
| Variable | Use |
|---|---|
--sidebar-item-action-size | Right-edge slot reserved size (matches button height so the slot fills the row vertically). A .button inside still collapses to a fixed square via an internal override. |
Group
| Variable | Use |
|---|---|
--sidebar-group-gap | Space between groups in the menu |
--sidebar-group-title-font-size | Caption text size |
--sidebar-group-title-font-weight | Caption weight |
--sidebar-group-title-color | Caption color (also tints .sidebar__group-action) |
Submenu
| Variable | Use |
|---|---|
--sidebar-submenu-border-color | Guide-line color |
--sidebar-submenu-padding-inline-start | Space between the guide line and submenu rows |
--sidebar-submenu-margin-inline-start | Inset from the panel edge; aligns the guide line on the parent icon's center axis |
Motion
| Variable | Use |
|---|---|
--sidebar-transition-duration | Rail-mode soft-hide and width transition. Zeroed under prefers-reduced-motion. |