Sidebar nav
Vertical navigation inside a sidebar
Intent
Sidebar nav is a vertical navigation list built for the sidebar chrome (or a drawer on mobile). It handles flat rows, grouped sections with static or collapsible headers, and nested sub-groups -- the common shapes for primary app navigation. For horizontal top-nav, use nav-link.
Basic usage
.sidebar-nav on a <nav>. Each
row is an <a class="sidebar-link"> with a
leading .sidebar-link-icon and a
.sidebar-link-text span. Mark the current page
with aria-current="page"
(P5).
.sidebar-link-text is a markup slot kept in place
for a deferred collapsed-sidebar mode that hides text and
centers the icon. It carries no styling today but should stay
in the markup so the collapsed mode can ship without markup
churn.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<a href="#" class="sidebar-link" aria-current="page">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
</svg>
<span class="sidebar-link-text">Dashboard</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<span class="sidebar-link-text">Projects</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span class="sidebar-link-text">Team</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span class="sidebar-link-text">Settings</span>
</a>
</nav>
</aside>Links with trailing meta
Add a .sidebar-link-meta span after the text slot
for a count, keyboard hint, or badge. The slot pushes to the
trailing edge via margin-inline-start: auto; its
contents style themselves -- reach for
.badge for counts,
a <kbd> with the .text-muted
utility for shortcut hints, or plain text for anything else.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
</svg>
<span class="sidebar-link-text">Inbox</span>
<span class="sidebar-link-meta badge is-neutral">12</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</svg>
<span class="sidebar-link-text">Search</span>
<kbd class="sidebar-link-meta text-muted text--1">⌘K</kbd>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
<span class="sidebar-link-text">Starred</span>
</a>
</nav>
</aside>Groups
Group related links under a static section label with
.sidebar-group wrapping the section,
.sidebar-group-label on the header, and
.sidebar-group-content around the rows. The
content wrapper carries the inline-start rail that anchors
disclosed rows to their label and lights up the trail to the
current page; the label sits flush above it.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<div class="sidebar-group">
<span class="sidebar-group-label">Workspace</span>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link" aria-current="page">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
<span class="sidebar-link-text">Overview</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
<span class="sidebar-link-text">Members</span>
</a>
</div>
</div>
<div class="sidebar-group">
<span class="sidebar-group-label">Personal</span>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="8" r="4"></circle>
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"></path>
</svg>
<span class="sidebar-link-text">Profile</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span class="sidebar-link-text">API keys</span>
</a>
</div>
</div>
</nav>
</aside>Collapsible groups
Swap the <div> for <details>
and the <span> for <summary>
to make a group collapsible. Native disclosure, no JavaScript.
The chevron rotates on open; the height animates via base
<details> styling (see
details). Add the
open attribute for the default-expanded state.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Workspace</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
<span class="sidebar-link-text">Overview</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
<span class="sidebar-link-text">Members</span>
</a>
</div>
</details>
<details class="sidebar-group">
<summary class="sidebar-group-label">Archive</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="21 8 21 21 3 21 3 8"></polyline>
<rect x="1" y="3" width="22" height="5"></rect>
<path d="M10 12h4"></path>
</svg>
<span class="sidebar-link-text">Completed projects</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span class="sidebar-link-text">Historical activity</span>
</a>
</div>
</details>
</nav>
</aside>Nested groups
Drop a .sidebar-group inside another for
sub-sections. Nested group labels lose the uppercase
small-caps treatment in favor of a quieter medium-weight
label, and nested links gain an indent so the hierarchy reads
at a glance.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Workspace</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
<span class="sidebar-link-text">Overview</span>
</a>
<details class="sidebar-group">
<summary class="sidebar-group-label">Archive</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Foo</a>
<a href="#" class="sidebar-link">Bar</a>
<a href="#" class="sidebar-link">Baz</a>
<details class="sidebar-group">
<summary class="sidebar-group-label">Completed projects</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Foo</a>
<a href="#" class="sidebar-link">Bar</a>
<a href="#" class="sidebar-link">Baz</a>
</div>
</details>
</div>
</details>
<div class="sidebar-group">
<span class="sidebar-group-label">Projects</span>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link" aria-current="page">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<span class="sidebar-link-text">Active</span>
</a>
<a href="#" class="sidebar-link">
<svg class="sidebar-link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
<span class="sidebar-link-text">Completed</span>
</a>
</div>
</div>
</div>
</details>
</nav>
</aside>Active branch trail
When a sidebar link carries aria-current="page",
every ancestor group's rail upgrades to the active color via
:has(), tracing the path from the root group
down to the active row. The link itself paints a primary
segment over the parent's rail at its own row height --
pinning "you are here" to the rail line.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Workspace</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Overview</a>
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Projects</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Active</a>
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Q2 2026</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Brief</a>
<a href="#" class="sidebar-link" aria-current="page">Roadmap</a>
<a href="#" class="sidebar-link">Retros</a>
</div>
</details>
<a href="#" class="sidebar-link">Archived</a>
</div>
</details>
<a href="#" class="sidebar-link">Members</a>
</div>
</details>
</nav>
</aside>Suppressing the rail
Add .no-border to any .sidebar-group
to hide the rail and indent for that group only. Useful when
a group reads better as a flat section header without the
vertical anchor -- for example, a quick-actions bin
alongside hierarchical groups.
<aside class="sidebar">
<nav class="sidebar-nav" aria-label="Primary">
<details class="sidebar-group" open="">
<summary class="sidebar-group-label">Workspace</summary>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Overview</a>
<a href="#" class="sidebar-link">Members</a>
<a href="#" class="sidebar-link">Settings</a>
</div>
</details>
<div class="sidebar-group no-border">
<span class="sidebar-group-label">Quick</span>
<div class="sidebar-group-content">
<a href="#" class="sidebar-link">Inbox</a>
<a href="#" class="sidebar-link">Search</a>
</div>
</div>
</nav>
</aside>Class reference
| Class | Role |
|---|---|
.sidebar-nav |
Outer nav container. Sits inside .sidebar as a scrolling flex child; pads its contents and declares the shared typography tokens. |
.sidebar-link |
Nav row on an <a>. Flex row with icon, text, and optional meta. Owns hover, focus-visible, and [aria-current="page"] state. |
.sidebar-link-icon |
Leading icon slot. Sized by --sidebar-icon-size (falls back to --icon-size); muted by default, takes primary color when the parent link is current. |
.sidebar-link-text |
Text slot. Markup placeholder kept in place for a deferred collapsed-sidebar mode; currently styleless. |
.sidebar-link-meta |
Trailing slot. Pushes to the inline-end edge; content styling is consumer's concern (badge, <kbd>, text utility, etc.). |
.sidebar-group |
Section group wrapper. <div> for a static group; <details> for a collapsible one. Holds a label and a content wrapper; nests for sub-group hierarchy. Add .no-border to suppress the rail on a single group. |
.sidebar-group-label |
Group header. <span> inside a static group; <summary> inside a <details> group (adds chevron + click-to-toggle). Sits flush above the content wrapper, outside the rail. |
.sidebar-group-content |
Disclosed-content wrapper. Carries the inline-start rail and inline indent for nested rows. The rail upgrades to the active-branch color via :has([aria-current="page"]); the current row paints a primary-colored marker over the rail. |
Customization
Sidebar nav's scoped tokens cover typography, edge inset, and
the rail chrome. --sidebar-link-font-size sets
row font size and lives on the outer .sidebar so
it cascades to both nav and footer links;
--sidebar-nav-group-font-size tunes group labels
(nav-only); --sidebar-icon-size controls the icon
slot (falls back to the global --icon-size).
--sidebar-edge-pad is the small inset between the
nav's left edge and the label/bare-link text, also applied as
margin-inline-start on the rail so the line stays aligned with
the label above it.
The rail on .sidebar-group-content reads from
--sidebar-rail-width and
--sidebar-rail-color; the active-branch upgrade
reads from --sidebar-rail-color-active. Both rail
colors are derived from --sidebar-bg via
color-mix with --color-text, so they
track any background override and stay visible in light and
dark modes without a separate rule. Override the rail tokens
directly to pin a fixed color or point them at
--color-primary for a louder trail. The
current-row segment marker reads from
--sidebar-rail-marker-color. Indent on disclosed
rows comes from --sidebar-group-indent. Add
.no-border to a single
.sidebar-group to suppress the rail and indent
for that group.
Full token list and defaults:
src/css/sidebar-nav.css.
Accessibility
-
Wrap in a
<nav>landmark witharia-label. The landmark lets screen readers jump to the navigation region; the label distinguishes multiple navs on a page ("Primary", "Footer", "In-page"). -
Use
aria-current="page". Not.is-active. The attribute drives both the visual state and the screen-reader announcement (P5). -
Collapsible groups are keyboard-accessible
natively.
<details>+<summary>handle focus, enter/space toggle, and announce expanded state without wiring. No custom element required. -
Icons are decorative.
aria-hidden="true"on each<svg class="sidebar-link-icon">keeps screen readers from reading glyph metadata or repeating the concept behind the text. The text slot carries the accessible name. -
Keyboard shortcut hints use real keys.
When a meta slot shows a shortcut (
⌘K,/), wrap it in<kbd>so assistive tech recognizes it as input-key content rather than plain text.
CSS Reference: Sidebar Nav
Grouped vertical navigation for sidebars and drawers
Source: src/css/sidebar-nav.css
Tokens
| Token | Default | Description |
|---|---|---|
--sidebar-nav-group-font-size | var(--ui-text-xs) | Font size for group labels |
--sidebar-edge-pad | var(--ui-pad-x-compact) | Inline-start padding for labels and top-level links |
--sidebar-rail-width | 1px | Thickness of the group rail |
--sidebar-rail-color | color-mix(in oklch, var(--sidebar-bg), var(--color-shift) 12%) | Rail resting color, derived from --sidebar-bg |
--sidebar-rail-color-active | color-mix(in oklch, var(--sidebar-bg), var(--color-shift) 28%) | Rail color when the group contains the current link |
--sidebar-rail-marker-color | var(--color-primary) | Color of the current-row segment marker |
--sidebar-group-indent | var(--ui-pad-x-snug) | Inline indent applied alongside the rail |
Slots
| Slot | Description |
|---|---|
.sidebar-group | Section group, either static div or collapsible details |
.sidebar-group-content | Disclosed-content wrapper with inline-start rail |
.sidebar-group-label | Section header label or collapsible summary |
Classes
| Class | Description |
|---|---|
.sidebar-nav | component root |
.sidebar-group.no-border | Per-group opt-out of the rail (and therefore the marker too, since the marker has nothing to overlap without the rail). |
CSS Reference: Sidebar Link
Row primitive for sidebar navigation links
Source: src/css/sidebar-nav.css
Tokens
| Token | Default | Description |
|---|---|---|
--sidebar-link-font-size | var(--ui-text-sm | Font size for sidebar links, set on .sidebar |
--sidebar-icon-size | var(--icon-size | Icon dimensions for the leading slot |
Slots
| Slot | Description |
|---|---|
.sidebar-link-icon | Leading icon slot |
.sidebar-link-meta | Trailing slot for counts, hints, or badges |
Classes
| Class | Description |
|---|---|
.sidebar-link | component root |