Sidebar

Side panel chrome

Intent

Sidebar is the fixed-width vertical panel that holds primary app navigation -- a brand lockup, the nav itself (sidebar-nav), a user profile or sign-out footer. It owns only the container chrome: width, background, border, scroll, header/footer slots. Contents are consumer-supplied.

Below 64rem viewport width the sidebar hides itself and a .sidebar-toggle button becomes the navigation entry point -- consumers wire the toggle to a drawer holding a copy of the sidebar's nav. See app layouts for the full shell pattern.

Basic usage

.sidebar on an <aside> (or any block element) wraps a sidebar-nav. Default width is 16rem, background matches --color-surface, and an inline-end border separates it from the main content column.

<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">
                <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>

A direct-child <header> or <footer> of the sidebar marks the top and bottom regions. Each gets its own separator border (bottom on the header, top on the footer) and sits flex-shrink: 0 so the middle nav scrolls under a fixed header and above a fixed footer. The common shape: brand lockup in the <header>, sidebar-nav in the middle, user profile or sign-out row in the <footer>.

<aside class="sidebar">
    <header>
        <strong>Acme</strong>
    </header>
    <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 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="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>
    <footer>
        <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">octetic</span>
        </a>
    </footer>
</aside>

Without the inline-end border

.sidebar.no-border drops the inline-end separator. Use when the sidebar sits against a surface that already provides visual separation (a card, a different background tone) and the border would read as redundant or harsh.

<aside class="sidebar no-border">
    <nav class="sidebar-nav" aria-label="Secondary">
        <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">
                <circle cx="12" cy="12" r="10"></circle>
            </svg>
            <span class="sidebar-link-text">Summary</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="M3 3h18v18H3z"></path>
            </svg>
            <span class="sidebar-link-text">Details</span>
        </a>
    </nav>
</aside>

Mobile toggle

Below 64rem viewport width, .sidebar hides itself via display: none and .sidebar-toggle (hidden on desktop) reveals as an inline-flex button. Consumers place the toggle in the mobile top bar and wire it to a <brio-drawer> holding a copy of the sidebar-nav for the mobile-only surface. The toggle uses native commandfor invocation -- no bespoke JS.

<button class="sidebar-toggle" type="button" commandfor="mobile-drawer" command="show-modal" aria-label="Open navigation">
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <line x1="3" y1="6" x2="21" y2="6"></line>
        <line x1="3" y1="12" x2="21" y2="12"></line>
        <line x1="3" y1="18" x2="21" y2="18"></line>
    </svg>
    <span>Menu</span>
</button>

The button is hidden in the demo above on wide viewports; narrow your browser below 64rem to see it revealed (and the page's own sidebar hidden). For the full shell shape -- sidebar on desktop, top bar + toggle + drawer on mobile -- see app layouts.

Reference

NameRole
.sidebar Fixed-width flex column. Sets width, background, inline-end border, vertical scroll. Hidden below 64rem.
<header> (direct child) Top region with a block-end separator border. flex-shrink: 0 so it stays above a scrolling nav. Slot keyed by element, not class.
<footer> (direct child) Bottom region with a block-start separator border. flex-shrink: 0 so it stays below a scrolling nav. Slot keyed by element, not class.
.sidebar.no-border Exception: drops the inline-end separator when the layout already provides visual separation.
.sidebar-toggle Hamburger button for mobile, hidden on desktop. Wire to a <brio-drawer> via commandfor.

Customization

Default width is 16rem via --sidebar-width. Full token list and defaults: src/css/sidebar.css.

Accessibility

  • <aside> is the conventional root. The element provides a complementary landmark. Since the sidebar typically holds primary navigation, the <nav> inside (from sidebar-nav) is what screen-reader users rotor-jump to; the outer <aside> is a secondary landmark for the region as a whole.
  • One nav source on mobile. When the desktop sidebar hides and a drawer takes over, the drawer holds a copy of the same sidebar-nav markup. The hidden sidebar is display: none so it's removed from the accessibility tree -- no duplicate-nav announcement. Wire with commandfor so the toggle button and drawer don't need custom focus management.
  • Name the toggle. The hamburger glyph alone is not announced meaningfully. aria-label="Open navigation" (or a localized equivalent) gives the button an accessible name. A visible "Menu" text inside the button is also fine -- pick one based on whether the label is visible.
  • Keyboard focus returns correctly. The drawer (via native <dialog> under the hood) traps focus while open and returns it to the toggle on close -- standard modal behavior, no extra wiring.

CSS Reference: Sidebar

Fixed-width side panel

Source: src/css/sidebar.css

TokenDefaultDescription
--sidebar-width16remWidth of the sidebar panel
--sidebar-bgvar(--color-surface)Background color for the sidebar
--sidebar-bordervar(--border-width-default) solid var(--color-border-subtle)Inline-end border of the panel
--sidebar-link-font-sizevar(--ui-text-sm)Font size for sidebar-link rows inside the panel
--sidebar-header-bordervar(--border-width-default) solid var(--color-border-subtle)Bottom border for the header region
--sidebar-footer-bordervar(--border-width-default) solid var(--color-border-subtle)Top border for the footer region
SlotDescription
> headerThe header section at the top of a sidebar
> footerThe footer secton at the bottom of a sidebar
.sidebar-toggleMobile toggle -- hidden on desktop, revealed below 64rem in place of the hidden sidebar. Styled like a minimal button; consumers wire the open action with commandfor targeting a <brio-drawer> by ID.
ClassDescription
.sidebarcomponent root
.no-borderRemoves the inline-end border