Sidebar Navigation

A whole app navigation rendered from a NavGroup[] — sections, rows, nested groups and the badges or shortcuts that trail each label. Twenty routes become one array instead of twenty near-identical blocks of JSX that drift apart. It paints no surface of its own: the panel colour, its border and the icon rail all belong to the Sidebar shell around it, and it never imports a router — pass renderLink to hand it your app's Link component. This is the configurable component; for whole shells you copy and edit — rail plus panel, floating card, plan-aware footer — see Sidebar Blocks under Patterns.

tsx
import { SidebarNav, type NavGroup } from "@pestoui/core"

Groups and rows

Every row is a NavNode: an id, a label, and usually an icon. activeId names the current page — the row takes the --sidebar-accent fill and its leading icon takes --selected, the green this system reserves for the selected state. A node with an href renders an anchor; one without renders a button, so a nav with no routing is still semantic. SidebarNav has to sit inside a SidebarProvider, which is where the panel widths and the collapse state live.

Sections

A group is a section. Give it a label for a heading, or set separated to draw a rule above it instead — the two are alternatives, which is what lets one component read both shapes. A rule above the first group would be an edge rather than a divider, so it is skipped. action drops a control beside the heading.

Counts, shortcuts and external rows

badge, shortcut and external all land in the same in-flow trailing slot, so a row can carry more than one without them stacking on top of each other. badge takes a number or a string, shortcut renders as a key cap, and external opens the link in a new tab and marks it with an arrow that steps along its own diagonal on hover. disabled greys the row and stops it being clicked.

Nested groups

A node with children becomes a collapsible branch: the row gets a chevron, the children indent under a guide line, and nesting keeps going for as many levels as the data has. defaultExpandedIds lists the branches open on mount; pass expandedIds with onExpandedChange instead to drive expansion from your own state — restoring it from a route, say.

Icon rail

Collapsing belongs to the shell — Sidebar collapsible="icon" plus a SidebarTrigger — but the nav is what changes shape. Labels, counts and shortcuts drop away, each row keeps its icon and gains a tooltip carrying the label it just lost, and a nested group moves its children into a right-hand flyout rather than losing them, which is what a bare SidebarMenuSub does at this width. The shell pins itself to the viewport, so the frame below stands in for the page; in an app it is the page.

SidebarUser is the account row the lg size was always for: avatar, name, one line of secondary text and a trailing affordance, all of it truncating correctly and surviving the collapse to a 36px rail. Put it in SidebarFooter under the nav; the nav itself scrolls in SidebarContent while the footer stays put.

Props

PropTypeDefaultDescription
groupsNavGroup[]—The whole nav as data. Sections render in array order.
activeIdstring—NavNode id of the current page. Router-agnostic — the nav never reads location itself.
defaultExpandedIdsstring[]—Branch ids open on first render. Expansion is uncontrolled after that.
expandedIdsstring[]—Controlled expansion. Pass with onExpandedChange to own the open set yourself.
onExpandedChange(ids: string[]) => void—Fires with the full set of open branch ids whenever one is toggled.
renderLink(node: NavNode, children: ReactNode) => ReactNode—Inject the app's link component. Return an element wrapping children; the nav slots it into the row.
size"sm" | "default" | "lg""default"Row height, passed straight through to the menu buttons. Sub-rows keep their own height.
aria-labelstring"Main"Names the <nav> landmark. Give a second nav on the same page a different label.
NavGroup idstring—React key for the section. Never rendered.
NavGroup labelReactNode—Section heading. Omit for an unlabelled group.
NavGroup separatedbooleanfalseDraw a rule above the group instead of labelling it. Ignored on the first group.
NavGroup actionReactNode—Control beside the heading — a New button, usually. Hidden on the icon rail.
NavGroup itemsNavNode[]—The rows in this section.
NavNode idstring—Identity for activeId, expansion and the React key. Must be unique across the whole nav.
NavNode labelReactNode—Row text. Also the tooltip on the icon rail and the heading of a nested flyout.
NavNode iconReactNode—Leading glyph. The one element that takes --selected when the row is current.
NavNode hrefstring—Renders the row as an anchor. Omit for a row that only toggles or dispatches.
NavNode badgenumber | string—Count or short status in the trailing slot.
NavNode shortcutstring—Keyboard hint in the trailing slot, rendered as a key cap — e.g. "⌘1".
NavNode externalbooleanfalseOpens in a new tab with rel="noreferrer" and trails an outward arrow.
NavNode disabledbooleanfalseDims the row and takes it out of the pointer path via aria-disabled.
NavNode childrenNavNode[]—Turns the row into a collapsible branch. Nests to any depth.

Pesto UI · 117 documented components