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.

Sidebar Toggle

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

VariableUse
--sidebar-bgPanel background
--sidebar-colorPanel foreground (brand and buttons inherit)
--sidebar-padding-blockTop and bottom padding on header / footer
--sidebar-padding-inlinePanel outer gutter; feeds the rail-width recipe
--sidebar-gapSpace between header, content, footer
--sidebar-widthExpanded 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-collapsedRail width applied under [data-collapsed]. Pair with --sidebar-width and the flip animates via --sidebar-transition-duration.

Brand

VariableUse
--sidebar-brand-colorBrand text color
--sidebar-brand-icon-sizeBrand icon dimensions; load-bearing for rail re-center math
--sidebar-brand-gapSpace between brand icon and wordmark

Button (item)

VariableUse
--sidebar-button-heightHard height
--sidebar-button-padding-inlineHorizontal 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-blockVertical padding; defaults to 0 since the row height owns the rhythm
--sidebar-button-radiusCorner radius
--sidebar-button-gapSpace between icon and label
--sidebar-button-font-weightLabel weight
--sidebar-button-colorRest text color
--sidebar-button-bg-hoverHover chip fill
--sidebar-button-color-hoverHover text color
--sidebar-button-bg-activeActive chip fill (aria-current="page" or data-state="active")
--sidebar-button-color-activeActive text color
--sidebar-button-icon-sizeIcon dimensions; load-bearing for the rail recipe and the chevron
--sidebar-button-icon-colorIcon color at rest and hover. Active rows flip the icon to --sidebar-button-color-active so the row reads unified.

Item action

VariableUse
--sidebar-item-action-sizeRight-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

VariableUse
--sidebar-group-gapSpace between groups in the menu
--sidebar-group-title-font-sizeCaption text size
--sidebar-group-title-font-weightCaption weight
--sidebar-group-title-colorCaption color (also tints .sidebar__group-action)

Submenu

VariableUse
--sidebar-submenu-border-colorGuide-line color
--sidebar-submenu-padding-inline-startSpace between the guide line and submenu rows
--sidebar-submenu-margin-inline-startInset from the panel edge; aligns the guide line on the parent icon's center axis

Motion

VariableUse
--sidebar-transition-durationRail-mode soft-hide and width transition. Zeroed under prefers-reduced-motion.