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>

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

ClassRole
.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 with aria-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

TokenDefaultDescription
--sidebar-nav-group-font-sizevar(--ui-text-xs)Font size for group labels
--sidebar-edge-padvar(--ui-pad-x-compact)Inline-start padding for labels and top-level links
--sidebar-rail-width1pxThickness of the group rail
--sidebar-rail-colorcolor-mix(in oklch, var(--sidebar-bg), var(--color-shift) 12%)Rail resting color, derived from --sidebar-bg
--sidebar-rail-color-activecolor-mix(in oklch, var(--sidebar-bg), var(--color-shift) 28%)Rail color when the group contains the current link
--sidebar-rail-marker-colorvar(--color-primary)Color of the current-row segment marker
--sidebar-group-indentvar(--ui-pad-x-snug)Inline indent applied alongside the rail
SlotDescription
.sidebar-groupSection group, either static div or collapsible details
.sidebar-group-contentDisclosed-content wrapper with inline-start rail
.sidebar-group-labelSection header label or collapsible summary
ClassDescription
.sidebar-navcomponent root
.sidebar-group.no-borderPer-group opt-out of the rail (and therefore the marker too, since the marker has nothing to overlap without the rail).

Row primitive for sidebar navigation links

Source: src/css/sidebar-nav.css

TokenDefaultDescription
--sidebar-link-font-sizevar(--ui-text-smFont size for sidebar links, set on .sidebar
--sidebar-icon-sizevar(--icon-sizeIcon dimensions for the leading slot
SlotDescription
.sidebar-link-iconLeading icon slot
.sidebar-link-metaTrailing slot for counts, hints, or badges
ClassDescription
.sidebar-linkcomponent root