Components / Structure
SideNav
Vertical navigation for an app or a docs site: links in titled sections, groups that expand, the current page marked, and a collapsed icon rail.
When to useLink to "When to use"
- The main navigation of an app or site with more than a handful of destinations.
Use something else when:
- there are two to five peer views on one page: Tabs
- the choices are commands, not places: Menu
- you need the whole layout with a top bar and a small-screen drawer: AppShell, which holds a SideNav
UsageLink to "Usage"
import { SideNav } from "@arshad-shah/cynosure";
<SideNav label="Main" items={items} current={route.id} />An item with href is a link; without it, a button that calls onNavigate. An item with items is a group. Entries with a label and items but no id are sections with a heading.
RoutersLink to "Routers"
renderLink renders each link with your router; spread the props it receives (href, className, aria-current, onClick, children). Or keep plain links and call event.preventDefault() in onNavigate to route on the client.
<SideNav renderLink={({ href, ...props }) => <RouterLink to={href} {...props} />} … />GroupsLink to "Groups"
Groups open and close with a click, Enter or Space. A group holding the current page opens by itself, also when the page changes.
Collapsed railLink to "Collapsed rail"
collapsed shows icons only. Labels appear in tooltips on hover and keyboard focus and stay the accessible names. Give top-level items icons. Groups do not open inside the rail; choosing one calls onCollapsedChange(false) and opens the group, so the navigation can widen.
In the rail an item's badge shrinks onto the top inline-end corner of its icon, and stays part of the item's name ("Search 12").
Activity barLink to "Activity bar"
A collapsed SideNav on its own, without AppShell, is an editor's activity bar. footer takes entries pinned to the bottom (Settings, a notifications action): the same items, sections and actions as items, after them in the Tab order. With a footer the navigation fills its container's height. placement="end" is for a rail on the inline end of the window, such as a secondary activity bar: the current item's indicator moves to the side facing the content and the tooltips open towards it.
<SideNav
label="Views"
collapsed
current={view}
onNavigate={setView}
items={[
{ id: "explorer", label: "Explorer", icon: database },
{ id: "search", label: "Search", icon: search, badge: <Badge tone="accent">12</Badge> },
]}
footer={[
{ kind: "action", id: "notify", label: "Notifications", icon: bell, onClick: openNotifications },
{ id: "settings", label: "Settings", icon: settings },
]}
/>Bottom barLink to "Bottom bar"
layout="bar" lays the top-level items out in one row, icon over label, for a phone's tab bar (AppShell's tabs). Section headings are left out, and a group is one item that calls onNavigate with its own id. Give it three to five items, each with an icon.
A primary actionLink to "A primary action"
One top-level entry may be an action instead of a destination: { kind: "action", id, label, icon, onClick }. In the bar it is a round accent button raised above the row, for a phone's "Add". It sits where it is placed in items, so put it in the middle of an odd number of entries to centre it.
import { home, inbox, plus } from "@arshad-shah/cynosure/icons";
<SideNav
label="Sections"
layout="bar"
current={route.id}
items={[
{ id: "today", label: "Today", icon: home },
{ kind: "action", id: "add", label: "Add", icon: plus, onClick: openNewTask },
{ id: "inbox", label: "Inbox", icon: inbox },
]}
/>In the column the same entry is a full-width accent Button with the icon and the label, and in the collapsed rail an accent IconButton. Give a navigation one action at most and keep it at the top level, not inside a section or a group.
PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
labelRequired | Accessible name of the navigation landmark, e.g. "Main". | string | |
itemsRequired | Items and sections ({ label, items }), in order. One entry may be a primary action ({ kind: "action", id, label, icon, onClick }): a button that sits where it is placed. | NavEntry[] | |
current | id of the page being shown: marked aria-current="page"; groups holding it start expanded. | string | |
onNavigate | Called when an item is chosen (links too: call event.preventDefault() to route on the client). | ((id: string, event: MouseEvent<HTMLElement>) => void) | |
renderLink | Render links with your router: (props) => <RouterLink to={props.href} {...props} />. Default: a plain link. | ((props: NavLinkProps) => ReactElement) | a plain link |
collapsed | Icon-only rail; labels show in tooltips and stay the accessible names. Give top-level items icons. Groups do not open inside the rail: choosing one calls onCollapsedChange(false) so you can widen the navigation. | boolean | |
onCollapsedChange | Called with false when a group is chosen in the collapsed rail. | ((collapsed: boolean) => void) | |
defaultExpanded | Group ids expanded at first (groups holding current always are). | string[] | |
layout | column (default): the vertical navigation. bar: one row of top-level items, icon over label, for a phone's bottom tab bar (AppShell's tabs). Section headings are left out, and a group is one item that calls onNavigate with its id (a bar has no room to open it). Give a bar three to five items, each with an icon. | "column" | "bar" | |
size | Height and text size of the items. Default "md". | Size | "md" |
placement | Which side of the content the navigation sits on. start (default): the inline start, as a left sidebar or activity bar. end: the inline end, as a secondary activity bar on the right; the current item's indicator moves to the inline end, facing the content, and the rail's tooltips open towards the inline start. | "start" | "end" | |
footer | Entries pinned to the bottom of the navigation, such as Settings or a notifications action: items, sections and actions, as in items, and in the same Tab order after them. The navigation then fills its container's height. In a bar they end the row. | NavEntry[] | |
className | Added to the root element, for layout. | string | |
style | Layout only. Restyle through --cy-side-nav-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
- Landmark: a
<nav>named bylabel. Each section is a list named by its heading. - Current page:
aria-current="page", the accent's soft fill and a bar on the inline start (the inline end withplacement="end"). - Keyboard: Tab and Shift+Tab move between items, then the footer's; Enter follows a link; Enter or Space presses a button or toggles a group (
aria-expanded). There is no arrow-key roving: every item is a Tab stop, as in any list of links. - Disabled items are
aria-disabledbuttons that do nothing. - Primary action: a button named by its
label. It is not a link, is neveraria-current(even when itsidequalscurrent) and does not callonNavigate. It is one Tab stop in its place among the items; Enter or Space presses it. In the bar and the rail the label also shows as a tooltip. - Touch: rows are at least 44px tall on touch screens (24px with a mouse). The bar's action button is 48px across by default, and its hit area is never under 44px on touch screens.
- Contrast: items, headings and the current item keep 4.5:1 in light and dark modes.
- Direction: indents, the current bar,
placementand the rail's badge follow the text direction.
ThemingLink to "Theming"
| Token | Sets |
|---|---|
--cy-side-nav-width | Width of the expanded navigation (240px) |
--cy-side-nav-rail-width | Width of the collapsed rail |
--cy-side-nav-radius | Corner radius of item highlights |
--cy-side-nav-action-size | Diameter of the round action button in the bar (48px) |
--cy-side-nav-action-lift | How far the action button rises above the bar's row (8px) |
createTheme({ components: { sideNav: { width: "280px" } } });RelatedLink to "Related"
AppShell · Tabs · Button · IconButton · Tooltip
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.