Bridge view

Sidebar Nav

A persistent navigation region pinned to a viewport edge — typically the inline-start (visual left in LTR) on desktop, collapsing to a drawer on mobile. Holds links to independent pages or top-level sections of an application. Distinct from Tabs (in-page content switching), MenuButton (transient action surface), and Drawer-as-navigation (the modal on-demand variant of this pattern). SidebarNav is the canonical pattern for multi-section apps with persistent global navigation.

When to use

Use

For persistent global navigation in multi-section apps — dashboards, admin panels, content management systems, documentation sites with multi-page topics. The user sees all top-level destinations at all times (or reveals them via the collapsible toggle), navigates between them via clicks/Enter, and the URL changes on activation.

Avoid

For in-page content switching — that is `Tabs`. For transient action menus — that is `MenuButton`. For contextual disclosure of secondary content — that is `Disclosure` or `Accordion`. For mobile-only navigation — combine with `Drawer[variant=navigation]` for the collapsible-on-narrow-viewports pattern.

Versus related

  • tabs

    `SidebarNav` navigates between independent pages (URLs change, content reloads); `Tabs` switches between content panels in the same page (URL stays the same in canonical implementations). The semantic distinction drives the role and the DOM: sidebar uses `<nav>` with anchor links; tabs use `tablist` with buttons.

  • drawer

    `Drawer[variant=navigation]` is the temporary modal-on-mobile counterpart of SidebarNav. They compose: persistent SidebarNav on desktop, Drawer for the same nav on mobile (collapses below `breakpoint.md`). The drawer-variant is invoked by a trigger (hamburger MenuButton).

  • menu-button

    `MenuButton` is a transient action surface invoked on demand; `SidebarNav` is a persistent navigation region always visible (or collapsibly visible). The role differentiates: `role="menu"` for MenuButton, `<nav>` for SidebarNav. Sidebar styled as menu-button is a common antipattern.

  • accordion

    `Accordion` discloses content within a single page; `SidebarNav` navigates between independent pages. Sidebars may contain accordion-style collapsible groups for nested navigation, but the leaf items are still anchor links — the accordion is structural within the sidebar, not the sidebar itself.

Highlight
Fig 1.1 · Sidebar Nav · Bridge view
Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  1. 01
    Figma

    SidebarNav drawn looking like vertical Tabs

    Code

    A nav landmark with anchor links navigating to URL routes

    Consequence

    Designers may use SidebarNav and Tabs interchangeably for "vertical list of links". Implementations following the Figma file may ship `role="tablist"` for what is semantically navigation; SR users hear "tab" and expect in-page panel switching, but activating navigates to a different URL. The mental model breaks.

    Correct

    Distinguish at the canonical level: SidebarNav navigates between independent pages (URLs change); Tabs switch between content panels in the same page (URL stays). The semantic distinction drives the role (`<nav>` vs `tablist`) and the DOM (anchors vs buttons-with-aria- controls).

  2. 02
    Figma

    Collapsed variant drawn with icons but no tooltip on hover

    Code

    Each item in collapsed variant has a Tooltip showing the label on hover or focus

    Consequence

    Designers compose collapsed-variant mocks with icon-only items. Implementations following the design ship without hover tooltips; pointer users hovering an icon see no label, must guess from the icon glyph. Keyboard users see no label either (focus does not reveal anything beyond the visible icon).

    Correct

    Collapsed-variant items always have a Tooltip on hover/ focus revealing the full label. The label remains in the DOM (visually-hidden) for SR users; the Tooltip is a sighted-user affordance that surfaces it visually. Document the pairing canonically.

  3. 03
    Figma

    Active page indicated by font-weight change alone

    Code

    Active page indicated by `aria-current="page"` PLUS background fill PLUS leading accent strip

    Consequence

    Designers use a single visual change (bold text) to mark active. Implementations following the design fail WCAG 1.4.1 (Use of Color and 1.3.3 Sensory Characteristics) — bold-only differentiation is not enough for SR users (without `aria-current`) and is unreliable for users with dyslexia or reduced contrast vision.

    Correct

    Triple-layer active-state differentiation: visual (background fill + leading accent stripe + text colour shift), accessible state (`aria-current="page"`), and typography (font-weight increase). The `aria-current="page"` is the source of truth; visuals reinforce.

  4. 04
    Figma

    Sidebar drawn at fixed-width with no overflow handling

    Code

    Sidebar with internal scroll for overflow; main content scrolls independently

    Consequence

    Designers fix the sidebar at one screen-height (typical desktop mock at 1080p). Implementations following the design break on shorter viewports — items below the fold are clipped, with no scroll affordance. Mobile especially affected.

    Correct

    Sidebar's internal item region scrolls independently from main content. Footer (when present) pins to the bottom; items between header (if present) and footer scroll. Document the canonical scroll-behaviour: never truncate items silently.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

expandedcollapsed

Properties

The same component, parameterised.

PropertyType
collapsibleboolean
hasFooterboolean
densitycomfortable | compact

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactive
data
idlecurrent-page
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps expanded / collapsed.
CollapsibleBooleancollapsibleToggles the user-facing collapse affordance (typically a chevron-button at the sidebar's inline-end edge).
DensityVariantdensitycomfortable / compact.
Has FooterBooleanfooter
Has HeaderBooleanheaderOptional top region for logo / app name. Separate from anatomy because it is not always present and is not a single canonical slot.
Item CountVariantitems.lengthFigma exposes 4/8/12/16+ item counts for preview; code accepts arbitrary item arrays.
Has GroupsBooleangroupsToggles whether items render as flat list or are categorised into labelled groups.
Active ItemVariantactiveItemIdFigma exposes "first / second / third item active" as Variant for preview; in code this is route-derived (current URL) or controlled prop.
Both

State transitions

FromToTrigger
idlecurrent-pagePage navigation lands the user on the route owned by this nav item. The item's `aria-current="page"` is set; visual treatment changes to indicate current-page state. Other items in the sidebar lose their current-page state (only one can be active at a time).
current-pageidleUser navigates to a different page. The previously- current item loses `aria-current="page"`; the new current item gains it.
Designer

Figma anatomy

Slot Figma type Hint
root frame Full-block-size auto-layout vertical frame; pinned to inline-start of viewport
group frame Vertical frame with optional label heading and items inside
group-label text Heading text style; uppercase or small-caps for category dividers
item instance Nav item component instance with active and inactive variants
item-icon instance Icon component instance per item; required in collapsed variant
item-label text Item text style; visibility property bound to expanded state
badge instance Badge component instance; visibility bound to "has notification" state
footer frame Auto-layout vertical frame pinned to bottom of sidebar
Dev

Code anatomy

Slot Code slot Semantic
root root nav-landmark
group group group-region
group-label group-label heading-or-strong
item item link
item-icon item-icon presentational-or-img
item-label item-label text
badge badge presentational-or-status
footer footer contentinfo-region
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-sidebar>` host wrapping a `<nav>` with internal `<ul>` of items; named slots for groups, footer, and the optional collapsible toggle attributes (`variant="collapsed"`, `collapsible`, `density="comfortable"`, `has-footer`); `data-state="expanded|collapsed"` for CSS
React Custom `<Sidebar>` component composing `<nav>`, item lists, and footer; React Aria does not ship a SidebarNav primitive but provides `useNavigationItem` for nav-item semantics; routing libraries (Next.js, React Router) integrate via active-route-aware item components props with class-variance-authority for variant / density; `collapsible` boolean drives the toggle; controlled expansion state for animations
Angular (signals) Angular Material `MatSidenav` plus `MatSidenavContainer` provides the layout pattern; or a custom directive on `<nav>` plus signal-based state input<'expanded' | 'collapsed'>(); `[collapsible]`, `[density]` host bindings
Vue Custom `<SidebarNav>` SFC composing `<nav>` with named slots for groups and footer; Vue Router's `<router-link>` provides active-route handling defineProps with literal-union types; `:variant`, `:collapsible` props
Both

Events

  1. variantChange
    Payload
    `{ variant: 'expanded' | 'collapsed' }`. Fires when the user toggles the sidebar variant via the collapsible affordance (when `collapsible: true`). Lets consumers persist the preference to localStorage.
    Web Components
    `variantChange` CustomEvent on the host with `event.detail = { variant }`.
    React
    `onVariantChange(variant)` controlled-pattern callback. Most implementations expose `expanded: boolean` instead of the variant enum.
    Angular Signals
    `output<SidebarVariant>('variantChange')`; pair with `[(variant)]` two-way binding.
    Vue
    `@update:variant` for `v-model:variant`.
  2. itemActivate
    Payload
    `{ itemId: string, href: string }`. Fires when the user activates an item (click, Enter on focused link). For route-driven sidebars wired to a router (React Router, Vue Router), the consumer typically does not handle this directly — the router handles navigation. Useful for analytics or for in-app routing without a router.
    Web Components
    `itemActivate` CustomEvent with `event.detail = { itemId, href }`.
    React
    Per-item `onClick` callbacks. Aggregate event uncommon because routing libraries already provide navigation-event hooks.
    Angular Signals
    `output<{ itemId: string, href: string }>('itemActivate')`.
    Vue
    `@item-activate` event with payload `{ itemId, href }`.
Both

Performance thresholds

  • maxItemsBeforeScrollvisible-item-count12items

    Above ~12 items visible without scrolling, the sidebar becomes scannable only by reading top-to-bottom; users cannot recall the structure. For deep navigation hierarchies, group items into collapsible sections (using nested Disclosure or Accordion patterns inside the sidebar) or split into multiple navs. Above 12 items, internal scroll is required; the canonical reference recommends scrollable item region with pinned header/footer.

  • navItemPaintBudgetper-item-paint-cost1ms

    Each sidebar item's paint cost should stay under 1ms to keep the total sidebar paint within the 16ms 60fps frame budget at typical item counts (12–40 items). Items with rich content (avatars, badges, multi-line labels) may exceed this; profile and simplify if sidebar paint dominates initial render.

Both

Internationalisation

RTL · mirroring

Sidebar pinning moves from inline-start (visual left in LTR) to inline-start (visual right in RTL). Item icon position reverses logically. Collapse-toggle chevron points away from the sidebar (right in LTR, left in RTL); the chevron glyph itself rotates to follow. Notification badges remain at inline-end of items (visual right in LTR, visual left in RTL).

Text expansion

Item labels grow significantly in DE / RU / FI — German "Einstellungen" is twice as wide as English "Settings". Sidebar inline-size grows to fit the longest label or truncates with ellipsis (full text in `aria-label` or via Tooltip). Collapsed variant is unaffected by text expansion (icons are direction-neutral and same width). Group labels follow item-label expansion patterns.

Both

Accessibility

Slot Accessibility hint
root Apply `<nav>` element with `aria-label="Primary"` (or equivalent — for apps with multiple navigations, label each distinctly). The nav landmark allows SR users to jump here via landmark navigation. Avoid stacking multiple `aria-label="Navigation"` regions on the same page; each should have a distinguishing label.
group Group is a structural grouping. The group label inside is a heading element (commonly h3 or h4 depending on document outline) when the label conveys hierarchical structure; or `aria-labelledby` referencing a non-heading label.
group-label Heading element when the label carries hierarchical meaning. Visually-hidden in collapsed variant via clip-path or sr-only class — never `display: none` (which removes from SR tree).
item Real `<a href="...">` with the target route. The currently-active item carries `aria-current="page"` — SR announces "current page" before the link text. Avoid `aria-current="true"` (less specific) or bespoke `data-active` (no SR signal). Disabled links are rendered as plain text or as `<a>` with `aria-disabled="true"` plus removed `href`.
item-icon Decorative when paired with a visible label (`aria-hidden="true"`). For collapsed variant where the label is visually-hidden, the icon is still decorative because the visually-hidden label remains in the DOM for SR users — the icon never needs an aria-label for collapsed-variant nav items.
item-label Plain text. The nav item's accessible name. In collapsed variant, hide visually with sr-only / clip- path technique — NEVER `display: none` (which removes from accessible tree, leaving an icon-only link with no accessible name).
badge For numeric badges, append the count to the link's accessible name via visually-hidden text or `aria-label` composition ("Inbox, 3 unread"). Decorative-only badges (a pulse for "new") use `aria-hidden="true"` with the link's accessible name carrying the meaning explicitly ("Settings (new)").
footer Footer content is part of the sidebar's nav landmark but acts as a sub-region. For user-account flyouts, the footer typically hosts a MenuButton (avatar + dropdown). For static info, plain content is sufficient.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
F6 (when supported by browser/OS)Cycles focus to landmark regions including the nav. Allows keyboard users to reach the sidebar without tabbing through earlier content.
TabFocus enters the first item in the sidebar. Subsequent Tab moves to the next focusable item; continues through the entire sidebar (groups, items, footer) in DOM order, then exits to the next document focusable.
Enter (focus on item)Activates the link — navigates to the `href`. Spacebar does not activate (anchors are Enter-only by native convention).
ArrowDown / ArrowUp (focus on item)Optional per APG — Polaris and Spectrum bind ArrowKeys for sidebar navigation; canonical reference marks this as enhancement, not required. Tab is the canonical key for moving between items.
Escape (focus inside sidebar, mobile drawer mode)Closes the drawer-variant sidebar; focus restores to the trigger that opened it. For non-drawer (persistent desktop) sidebar, Escape is a no-op.

Screen-reader announcements

TriggerExpected
Focus enters the sidebarSR announces "Primary navigation" (or whatever the nav's `aria-label` is) followed by the focused item. For F6 landmark navigation, the announcement precedes any item interaction.
Focus enters the current-page itemSR announces "<item label>, current page, link". The current-page portion comes from `aria-current="page"`. Other items announce as "<label>, link" without the current-page prefix.
Item activated, navigation occursNew page loads; focus typically returns to the sidebar item that was clicked (browsers vary). SR announces the new page's title and main heading; navigation is implicit.
Item with notification badgeSR announces "<item label>, <count> unread, link" or "<item label> (new), link" depending on whether the badge is numeric or pulse-only. Driven by visually- hidden text composed into the link's accessible name, not by `aria-label` (which would lose the link text).

axe-core rules to assert

  • aria-allowed-role
  • aria-required-attr
  • aria-current
  • color-contrast
  • link-name
  • landmark-unique
  • region
Both

Common mistakes

#sidebar-no-aria-current

Active page not marked with `aria-current`

Problem

The visual current-page state is shown (highlight, icon-fill change, accent stripe) but `aria-current` is missing from the active link. SR users hear no indication of which page they are currently on; users relying on screen reader for orientation are lost.

Fix

The active item carries `aria-current="page"` (most specific value for navigation links). SR announces "current page" before the link text. Update on every navigation.

#sidebar-collapsed-display-none-label

Collapsed variant uses `display: none` to hide labels

Problem

Implementation hides labels in collapsed variant via `display: none`. The labels are removed from the accessibility tree; SR users hear icon-only links with no accessible name (the icon's aria-hidden is correct but now there is nothing else for the link's name).

Fix

Hide labels via the `sr-only` / clip-path technique that keeps content in the accessibility tree but visually removes it. The link's accessible name remains the label text; SR users hear "Settings, link" while pointer users see only the icon.

#sidebar-as-menubutton

Sidebar implemented as a Menu (role="menu")

Problem

Implementation uses `role="menu"` and `role="menuitem"` on the sidebar items. SR users hear "menu" and expect menu-keyboard semantics (ArrowKeys auto-activate); activation navigates to a URL, breaking the menu's "select one command" mental model. Tab is captured by menu semantics rather than allowing escape.

Fix

SidebarNav uses `<nav>` with anchor links (or a `<ul>` of `<li>` containing `<a>`). Not menu, not menuitem, not menubutton. Tab navigates through items as independent links; ArrowKeys are not bound canonically (Polaris and Spectrum do bind them as an enhancement, but it is optional, not required by APG).

#sidebar-mobile-no-collapse

Sidebar always rendered on mobile

Problem

Sidebar takes 240px of viewport width on a 360px-wide mobile device. Main content gets 120px and is unusable. Or sidebar collapses to icon-only without trigger to re-expand.

Fix

On mobile (and below `breakpoint.md`), sidebar collapses to an off-canvas Drawer pattern — invisible by default, summoned via a trigger (typically a hamburger menu-button in the page header). The Drawer becomes the sidebar at narrow widths. Document the responsive transition to Drawer in `responsive.breakpoints`.