Skip to content

Sidebar

App navigation: a brand block, a search slot, labelled sections of links with icons, counts, badges and expandable groups, a footer with the signed-in user, and an icon-rail collapsed mode.

Preview

Installation

pnpm add @syntara/react @syntara/tokens
import { Sidebar, SidebarHeader, SidebarSearch, SidebarSection, SidebarItem, SidebarFooter, SidebarUser, useSidebar } from '@syntara/react';

Usage

import { IconTile, Menu, MenuItem, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarSearch, SidebarSection, SidebarUser } from '@syntara/react';
import { IconLayoutGrid, IconSettings, IconSparkles, IconUsers } from '@syntara/icons';

export function AppNav({ collapsed, onCollapsedChange, openSearch }) {
  return (
    <Sidebar aria-label="Main" variant="floating" collapsed={collapsed} onCollapsedChange={onCollapsedChange}>
      <SidebarHeader logo={<IconTile tint="none"><IconSparkles /></IconTile>} title="Tempo" subtitle="Plan the team's week" />
      <SidebarSearch shortcut="⌘K" onPress={openSearch} />
      <SidebarSection title="General">
        <SidebarItem href="/" icon={<IconLayoutGrid />}>Overview</SidebarItem>
        <SidebarItem icon={<IconUsers />} label="Employees" count={2}>
          <SidebarItem href="/people/jonah">Jonah Adams</SidebarItem>
          <SidebarItem href="/people/yuri" isCurrent>Yuri Jackson</SidebarItem>
        </SidebarItem>
      </SidebarSection>
      <SidebarFooter>
        <SidebarItem href="/settings" icon={<IconSettings />}>Settings</SidebarItem>
        <SidebarUser name="Maya Chen" description="maya@example.com" menu={<Menu><MenuItem id="out">Sign out</MenuItem></Menu>} />
      </SidebarFooter>
    </Sidebar>
  );
}

Examples

Collapsed

The icon rail (controlled). Tooltips name every icon, counts become a dot, and the Employees group opens its people in a popover. The toggle sits under the avatar.

Flush

Full-height app chrome with a hairline at the inline end, a search launcher, and a button item (onPress). Without a SidebarUser the toggle gets its own row.

5 unread messages

Upgrade card

A feature Card in SidebarFooter as the upgrade prompt.

Narrow screens

The same Sidebar inside a start-side Sheet, opened from a menu button.

Accessibility

KeysAction
TaborShiftTabMoves through the search, every item (and the links of open groups) in order, then the footer items, the account menu button and the collapse toggle.
EnterFollows a link item; activates a button item; opens or closes a group.
SpaceActivates a button item, the collapse toggle and the ⋯ button; opens or closes a group.
EscapeCloses an open tooltip, the collapsed group's popover or the account menu, returning focus to its trigger.
Arrow keysInside the account menu only: move between its items (React Aria Menu).
  • The sections render in a <nav> landmark named by aria-label; each section is a <ul role="list"> named by its title. SidebarHeader, SidebarSearch and SidebarFooter sit outside the landmark.
  • No roving focus, on purpose: this is site navigation, a list of links, not a menu or a tree (WAI-ARIA's navigation pattern uses the Tab sequence). React Aria's NavigationTree was considered and not used for groups: it's a tree with arrow-key focus, which would break the Tab-through list of links.
  • Groups use React Aria's Disclosure: the parent is a button with aria-expanded and aria-controls, and the panel is a group named by it. Closed groups get hidden="until-found", so their links leave the tab order. A group holding the current page opens by default.
  • Collapsed groups: the parent icon opens a Popover dialog (named by the group's label) holding the same links, so links stay links and keep aria-current. Focus moves in, Escape closes it and returns focus. We chose this over 'expand the whole sidebar', which would move every other item under the pointer.
  • The current page is aria-current="page" on the link, shown by the raised pill and medium weight together (plus the brand marker when nested), never colour alone.
  • Collapsed mode visually hides labels, counts, badges and section titles, but they stay in the DOM, so names and list labels don't change. Each icon gets a Tooltip on hover and keyboard focus; the search button's tooltip includes the shortcut.
  • The collapse toggle lives beside SidebarUser (under the avatar on the rail). Its label says what it will do ("Collapse sidebar" / "Expand sidebar") and it has aria-controls pointing at the nav. It doesn't also set aria-expanded, so the state isn't announced twice.
  • SidebarSearch in launcher mode is a button (it opens something), not a text input that opens a dialog on focus. The ⌘K hint is aria-hidden display text; bind the shortcut yourself.
  • Narrow screens: put the Sidebar (without its header) inside <Sheet side="start"> opened from a menu button; the Sheet gives the modal focus trap, Escape and the title. See the sidebar-mobile example.

Guidelines

Do

  • Give every top-level item an icon, so the collapsed mode works.
  • Keep section titles to one word ("General", "Other").
  • Use nested items for a short list of related pages (people, projects); keep it to one level.
  • Mark exactly one item isCurrent.
  • Bind the shortcut you show in SidebarSearch.

Don’t

  • Don't nest groups inside groups.
  • Don't make a group's parent a page as well; give it an Overview child instead.
  • Don't add brand fills to items: the brand marker on the current nested item is the only brand colour in the nav.
  • Don't hide the Sidebar on narrow screens without offering it in a Sheet.

API reference

Sidebar

aria-labelRequired
string

Names the nav landmark (e.g. "Main"). Required because a page can have several navs.

variant
  • flushdefault
  • floating

flush = full-height chrome with a hairline at the inline end; floating = an inset panel: the canvas tone (darker than the page in dark), container radius, hairline edge, generous padding.

collapsed
boolean

Icon-only mode (controlled). Labels, section titles and counts are visually hidden but stay in the accessible names; labels show in tooltips.

defaultCollapsed
boolean

Initial icon-only mode (uncontrolled). Setting it shows the toggle.

onCollapsedChange
(collapsed: boolean) => void

Called by the collapse toggle (and by the collapsed SidebarSearch in field mode). Setting it shows the toggle, which sits beside SidebarUser (or in its own row at the bottom when there's no SidebarUser).

collapsible
boolean

Force the collapse toggle on or off. Defaults to true when onCollapsedChange or defaultCollapsed is set.

collapseLabel
string

Toggle label while expanded. Localise it.

Default 'Collapse sidebar'

expandLabel
string

Toggle label while collapsed. Localise it.

Default 'Expand sidebar'

SidebarHeader

logo
ReactNode

Brand mark, e.g. an IconTile. Stays visible when collapsed.

title
ReactNode

Product name.

subtitle
ReactNode

A small line under the title (workspace, plan).

SidebarSearch

aria-label
string

Names the field (and the icon button when collapsed). Localise it.

Default 'Search'

placeholder
string

Placeholder text.

Default 'Search…'

shortcut
string

A Kbd hint at the end, e.g. "⌘K". Display only: bind the keys yourself (see sidebar-demo).

onPress
(e: PressEvent) => void

Launcher mode: the field is a button that calls this (e.g. to open a CommandDialog), and so is the collapsed icon button. Without it the field is a SearchField (value/onChange/onSubmit pass through) and the collapsed button expands the sidebar and focuses it.

SidebarSection

title
ReactNode

Quiet label above the items; names the list (aria-labelledby). Visually hidden when collapsed.

SidebarItem

href
string

Makes the item a link (React Aria Link; client routers via RouterProvider). Without it the item is a button: pass onPress.

onPress
(e: PressEvent) => void

For button items (no href), e.g. "Invite people".

icon
ReactNode

Leading icon from @syntara/icons. Needed for the collapsed mode. Nested items without one get a small square bullet.

isCurrent
boolean

The current page: aria-current="page" and the neutral raised pill (surface.raised, hairline, sheen). Nested items also get their bullet in text.brand and a small brand marker on the guide line. A group holding the current page opens by default, and shows the pill on the collapsed rail.

Default false

count
number

Trailing count, formatted for the locale, part of the accessible name ("Statements 3"). A dot on the icon when collapsed.

badge
ReactNode

Trailing badge. A string becomes a soft neutral Badge ("New", "Beta"); pass a Badge for another tone.

isDisabled
boolean

Not focusable or pressable; text.disabled.

Default false

childrenRequired
ReactNode

The label, or nested SidebarItems: then the item is an expandable group (a React Aria Disclosure; a popover dialog on the collapsed rail). A group is always a button, never a link.

label
ReactNode

A group's label when its children are nested items. Plain children before the items work too.

defaultExpanded
boolean

Group only: initially open. Defaults to true when a nested item isCurrent.

isExpanded
boolean

Group only: open (controlled).

onExpandedChange
(isExpanded: boolean) => void

Group only: called when the group opens or closes.

SidebarFooter

children
ReactNode

Pinned to the bottom behind a hairline, outside the nav landmark: SidebarItems (gathered into a list), then a SidebarUser. Use useSidebar().collapsed for custom content.

SidebarUser

nameRequired
string

The person's name, in medium weight. Avatar initials and the collapsed avatar button's name come from it.

description
ReactNode

A second line in text.subtle, e.g. the email address or plan.

src
string

Avatar photo; initials without it.

menu
ReactElement

A <Menu> of account actions, opened by the ⋯ button (by the avatar when collapsed). Children work too.

menuLabel
string

Accessible name of the ⋯ button. Localise it.

Default 'Account options'

Tokens

The semantic tokens this component reads, grouped by what they control. Swatches show this site’s theme; change a tenant’s brand and the component follows with no code change.

Colour9
border.subtlefocus.ringsurface.canvassurface.defaultsurface.raisedtext.brandtext.defaulttext.disabledtext.subtle
Type15
font.bodyfont.headingfont.heading-trackingfont.size.mdfont.size.smfont.size.xsfont.tracking.mdfont.tracking.smfont.tracking.xsfont.weight.mediumfont.weight.regularfont.weight.semiboldline-height.normalline-height.snugline-height.tight
Space and size9
control-heighticon-strokespace.1space.16space.2space.3space.4space.5space.6
Shape4
radius.badgeradius.buttonradius.containerradius.pill
Depth3
hairlineshadow.highlightshadow.raised
Motion7
motion.duration.fastmotion.duration.normalmotion.duration.slowmotion.duration.springmotion.easingmotion.easingOutmotion.spring