Designer 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.
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 |
Token usage per slot
root- spacing
- padding
spacing.compact - gap
spacing.tight
- padding
- color
- background
color.surface.sunken - border
color.border.subtle
- background
group- spacing
- gap
spacing.tight
- gap
group-label- color
- foreground
color.text.muted
- foreground
- typography
- size
text.xs - weight
weight.medium - tracking
tracking.wide
- size
item- spacing
- padding
spacing.compact - gap
spacing.compact
- padding
- radius
- corner
radius.sm
- corner
- color
- foreground
color.text.primary - ring
color.border.focus
- foreground
- typography
- size
text.sm
- size
item-icon- color
- foreground
color.text.muted
- foreground
item-label- color
- foreground
color.text.primary
- foreground
- typography
- size
text.sm
- size
badge- spacing
- padding
spacing.tight
- padding
- radius
- corner
radius.pill
- corner
- color
- background
color.accent.bg - foreground
color.accent.fg
- background
- typography
- size
text.xs - weight
weight.semibold
- size
footer- spacing
- padding
spacing.compact - gap
spacing.tight
- padding
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps expanded / collapsed. |
Collapsible | Boolean | collapsible | Toggles the user-facing collapse affordance (typically a chevron-button at the sidebar's inline-end edge). |
Density | Variant | density | comfortable / compact. |
Has Footer | Boolean | footer | — |
Has Header | Boolean | header | Optional top region for logo / app name. Separate from anatomy because it is not always present and is not a single canonical slot. |
Item Count | Variant | items.length | Figma exposes 4/8/12/16+ item counts for preview; code accepts arbitrary item arrays. |
Has Groups | Boolean | groups | Toggles whether items render as flat list or are categorised into labelled groups. |
Active Item | Variant | activeItemId | Figma exposes "first / second / third item active" as Variant for preview; in code this is route-derived (current URL) or controlled prop. |
Motion
| Transition | Duration token |
|---|---|
expandToggle | motion.duration.base |
itemHover | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.md | At and below, the sidebar collapses to an off-canvas Drawer pattern — invisible by default, summoned via a hamburger MenuButton trigger in the page header. The Drawer's `variant: navigation` is the canonical modal-mobile-sidebar pattern. Above `breakpoint.md` the sidebar is persistent. |
breakpoint.lg | Above this width, the authored variant (expanded vs collapsed) renders persistently. Some apps use breakpoint.xl as the auto-expand threshold (compact sidebars on tablet, expanded on desktop); others leave it as a user preference toggled via the collapsible affordance. |
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.
Variants, properties, states
Variants
Structurally different versions of the component.
expandedcollapsed Properties
The same component, parameterised.
| Property | Type |
|---|---|
collapsible | boolean |
hasFooter | boolean |
density | comfortable | compact |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactive |
data | idlecurrent-page |
State transitions
| From | To | Trigger |
|---|---|---|
idle | current-page | Page 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-page | idle | User navigates to a different page. The previously- current item loses `aria-current="page"`; the new current item gains it. |
Figma↔Code mismatches
- 01 Figma
SidebarNav drawn looking like vertical Tabs
CodeA nav landmark with anchor links navigating to URL routes
ConsequenceDesigners 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.
CorrectDistinguish 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).
- 02 Figma
Collapsed variant drawn with icons but no tooltip on hover
CodeEach item in collapsed variant has a Tooltip showing the label on hover or focus
ConsequenceDesigners 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).
CorrectCollapsed-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.
- 03 Figma
Active page indicated by font-weight change alone
CodeActive page indicated by `aria-current="page"` PLUS background fill PLUS leading accent strip
ConsequenceDesigners 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.
CorrectTriple-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.
- 04 Figma
Sidebar drawn at fixed-width with no overflow handling
CodeSidebar with internal scroll for overflow; main content scrolls independently
ConsequenceDesigners 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.
CorrectSidebar'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.
Common mistakes
#sidebar-no-aria-current
Active page not marked with `aria-current`
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.
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
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).
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")
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.
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-no-skip-link
Sidebar precedes main content without skip-to-main
Sidebar appears first in the DOM. Keyboard users tabbing into the page must traverse every sidebar link before reaching main content. On a sidebar with 20+ items, this adds 20+ Tab presses for every interaction with main content.
Document-level "Skip to main content" link as the first focusable element on the page (typically visually-hidden until focused). Document this as a sibling of the SidebarNav; it is the page's responsibility, not the sidebar's, but the sidebar amplifies the need.
#sidebar-mobile-no-collapse
Sidebar always rendered on mobile
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.
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`.
Accessibility hints
| 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. |