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/tokensimport { 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.
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
| Keys | Action |
|---|---|
| TaborShiftTab | Moves 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. |
| Enter | Follows a link item; activates a button item; opens or closes a group. |
| Space | Activates a button item, the collapse toggle and the ⋯ button; opens or closes a group. |
| Escape | Closes an open tooltip, the collapsed group's popover or the account menu, returning focus to its trigger. |
| Arrow keys | Inside 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-labelRequiredstringNames the nav landmark (e.g. "Main"). Required because a page can have several navs.
variantflushdefaultfloating
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.
collapsedbooleanIcon-only mode (controlled). Labels, section titles and counts are visually hidden but stay in the accessible names; labels show in tooltips.
defaultCollapsedbooleanInitial icon-only mode (uncontrolled). Setting it shows the toggle.
onCollapsedChange(collapsed: boolean) => voidCalled 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).
collapsiblebooleanForce the collapse toggle on or off. Defaults to true when onCollapsedChange or defaultCollapsed is set.
collapseLabelstringToggle label while expanded. Localise it.
Default
'Collapse sidebar'expandLabelstringToggle label while collapsed. Localise it.
Default
'Expand sidebar'
SidebarHeader
logoReactNodeBrand mark, e.g. an IconTile. Stays visible when collapsed.
titleReactNodeProduct name.
subtitleReactNodeA small line under the title (workspace, plan).
SidebarSearch
aria-labelstringNames the field (and the icon button when collapsed). Localise it.
Default
'Search'placeholderstringPlaceholder text.
Default
'Search…'shortcutstringA Kbd hint at the end, e.g. "⌘K". Display only: bind the keys yourself (see sidebar-demo).
onPress(e: PressEvent) => voidLauncher 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
titleReactNodeQuiet label above the items; names the list (aria-labelledby). Visually hidden when collapsed.
SidebarItem
hrefstringMakes the item a link (React Aria Link; client routers via RouterProvider). Without it the item is a button: pass onPress.
onPress(e: PressEvent) => voidFor button items (no href), e.g. "Invite people".
iconReactNodeLeading icon from @syntara/icons. Needed for the collapsed mode. Nested items without one get a small square bullet.
isCurrentbooleanThe 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
falsecountnumberTrailing count, formatted for the locale, part of the accessible name ("Statements 3"). A dot on the icon when collapsed.
badgeReactNodeTrailing badge. A string becomes a soft neutral Badge ("New", "Beta"); pass a Badge for another tone.
isDisabledbooleanNot focusable or pressable; text.disabled.
Default
falsechildrenRequiredReactNodeThe 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.
labelReactNodeA group's label when its children are nested items. Plain children before the items work too.
defaultExpandedbooleanGroup only: initially open. Defaults to true when a nested item isCurrent.
isExpandedbooleanGroup only: open (controlled).
onExpandedChange(isExpanded: boolean) => voidGroup only: called when the group opens or closes.
SidebarFooter
childrenReactNodePinned 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
nameRequiredstringThe person's name, in medium weight. Avatar initials and the collapsed avatar button's name come from it.
descriptionReactNodeA second line in text.subtle, e.g. the email address or plan.
srcstringAvatar photo; initials without it.
menuReactElementA <Menu> of account actions, opened by the ⋯ button (by the avatar when collapsed). Children work too.
menuLabelstringAccessible 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