Design System / 07.3
Navigation
Patterns for moving through pages, switching views, opening responsive menus, and finding commands.
Breadcrumbs
A small mono trail with aria-hidden slash separators. Ancestor links self-dim on hover; the last item is the medium-weight current page — never a link.
Live specimen
Rendered output
API reference
Inputs, types, and behavior
- Inputitems
- Type{ label, href? }[]
- BehaviorLast item renders as the medium-weight current page (aria-current="page"); an ancestor without href stays a static span.
- Inputlabel
- Typestring = "Breadcrumb"
- BehaviorAccessible name for the nav landmark. Render-only — server-safe.
- InputclassName
- Typestring?
- BehaviorMerged last onto the nav. An empty items array renders nothing.
Tabs
Uppercase labels on a hairline rail. The active tab holds full ink and draws a 1px underline in from the left; the underline retracts faster than it appears. Arrow keys move and activate with wrap; panels stay mounted so their state survives switching.
Live specimen
Rendered output
The selected panel remains available in source order.
Keyboard focus follows the established tab pattern.
Reduced motion keeps every state complete and still.
API reference
Inputs, types, and behavior
- Inputitems
- Type{ label, content }[]
- BehaviorKeys and ARIA ids append the index, so duplicate labels are safe.
- Inputindex / defaultIndex
- Typenumber? / number = 0
- BehaviorControlled active index or uncontrolled initial index; out-of-range values clamp to the first tab.
- InputonIndexChange
- Type(index) => void?
- BehaviorFires when automatic activation changes the active index.
- Inputlabel
- Typestring = "Tabs"
- BehaviorAccessible name for the tablist.
- InputclassName
- Typestring?
- BehaviorMerged onto the root wrapper.
Fluid tabs
The Tabs pattern with a single hairline underline that slides between tabs instead of redrawing in place — one measured 1px segment whose width snaps before its transform-only travel. Same props and full ARIA tabs keyboard support as Tabs; reduced motion snaps the indicator instead of sliding it.
Live specimen
Rendered output
The tablist exposes one selected tab and a matching panel while keeping every label readable.
Arrow keys move focus; Home and End reach the first and last available tabs.
Reduced motion removes interpolation without hiding the selected panel or its controls.
Malformed indexes and duplicate labels settle to deterministic, unique tab and panel identifiers.
API reference
Inputs, types, and behavior
- Inputitems
- Type{ label, content }[]
- BehaviorSame shape as Tabs; keys and ARIA ids append the index, so duplicate labels are safe.
- Inputindex / defaultIndex
- Typenumber? / number = 0
- BehaviorControlled active index or uncontrolled initial index; out-of-range values clamp to the first tab.
- InputonIndexChange
- Type(index) => void?
- BehaviorFires when click or keyboard input changes the active index.
- Inputlabel
- Typestring = "Tabs"
- BehaviorAccessible name for the tablist; override it when several tabsets share a page.
- InputclassName
- Typestring?
- BehaviorMerged onto the root wrapper.
Vertical tabs
The same sliding-hairline mechanic, rotated 90°: a tablist column at the inline start with the indicator height snapping before it moves down the edge, with the panel beside it. Stacks to tablist-above-panel below the sm breakpoint. Up/Down arrows move and activate; the list carries aria-orientation="vertical".
Live specimen
Rendered output
The tablist exposes one selected tab and a matching panel while keeping every label readable.
Arrow keys move focus; Home and End reach the first and last available tabs.
Reduced motion removes interpolation without hiding the selected panel or its controls.
Malformed indexes and duplicate labels settle to deterministic, unique tab and panel identifiers.
API reference
Inputs, types, and behavior
- Inputitems
- Type{ label, content }[]
- BehaviorSame shape as Tabs; keys and ARIA ids append the index, so duplicate labels are safe.
- Inputindex / defaultIndex
- Typenumber? / number = 0
- BehaviorControlled active index or uncontrolled initial index; out-of-range values clamp to the first tab.
- InputonIndexChange
- Type(index) => void?
- BehaviorFires when click or keyboard input changes the active index.
- Inputlabel
- Typestring = "Tabs"
- BehaviorAccessible name for the tablist; override it when several tabsets share a page.
- InputclassName
- Typestring?
- BehaviorMerged onto the root wrapper.
Spotlight tabs
Tabs with the crisp active-tab underline plus a dim hairline preview that slides to the hovered tab and returns to the active tab when the pointer leaves. Move a mouse across the tabs to see it. The preview only appears for fine pointers and is off under reduced motion — touch and other coarse pointers show just the active underline.
Live specimen
Rendered output
The tablist exposes one selected tab and a matching panel while keeping every label readable.
Arrow keys move focus; Home and End reach the first and last available tabs.
Reduced motion removes interpolation without hiding the selected panel or its controls.
Malformed indexes and duplicate labels settle to deterministic, unique tab and panel identifiers.
API reference
Inputs, types, and behavior
- Inputitems
- Type{ label, content }[]
- BehaviorSame shape as Tabs; keys and ARIA ids append the index, so duplicate labels are safe.
- Inputindex / defaultIndex
- Typenumber? / number = 0
- BehaviorControlled active index or uncontrolled initial index; out-of-range values clamp to the first tab.
- InputonIndexChange
- Type(index) => void?
- BehaviorFires when click or keyboard input changes the active index.
- Inputlabel
- Typestring = "Tabs"
- BehaviorAccessible name for the tablist; override it when several tabsets share a page.
- InputclassName
- Typestring?
- BehaviorMerged onto the root wrapper.
Prev / next
Entry-to-entry pagination under a hairline top border: eyebrow, title, and a stroke chevron. If one side is missing, its slot stays empty so the other side keeps its position.
Live specimen
Rendered output
API reference
Inputs, types, and behavior
- Inputprev / next
- Type{ href, title, eyebrow? }?
- BehaviorEyebrows default to "Prev" / "Next". Either side may be omitted.
- Inputlabel
- Typestring = "Pagination"
- BehaviorAccessible nav-landmark name; make it unique when several pagers share a page.
- InputclassName
- Typestring?
- BehaviorMerged last onto the nav wrapper.
View switch
A text-only List/Grid switcher. Both options rest at readable ink and self-dim on hover; the active choice keeps a hairline underline, medium weight, and aria-pressed. Controlled and generic.
Live specimen
Rendered output
- Grid
- List
- Index
- Detail
API reference
Inputs, types, and behavior
- Inputoptions
- Typereadonly T[]
- BehaviorOption labels; T extends string, so union state typechecks.
- Inputvalue / onValueChange
- TypeT / (v: T) => void
- BehaviorControlled pair. Buttons carry aria-pressed.
- InputclassName
- Typestring?
- BehaviorMerged onto the root flex container.
Brand wordmark
Wordmark is the brand lockup with scroll-compact motion: the label collapses to just the mark once you scroll past a threshold, and re-expands at the top or on hover/focus. Width changes snap around a 380ms enter / 220ms exit, so only transform and opacity animate. You supply your own mark — only the motion comes from the component. It inherits currentColor, so it reads ink on paper and paper on ink. This site’s own Logo and the NavBar brand are built on it.
Live specimen
Rendered output
First: collapses past 32px, then re-expands on hover or focus. Second: static via collapse=false, with a non-string label and explicit ariaLabel.
<Wordmark href="/" mark={<YourMark />} label="YOUR MARK" />
// static (no scroll collapse):
<Wordmark mark={<YourMark />} label="YOUR MARK" collapse={false} />API reference
Inputs, types, and behavior
- Inputlabel / mark
- TypeReactNode / ReactNode?
- BehaviorRequired wordmark plus optional currentColor mark. A non-string linked label requires ariaLabel.
- Inputhref / ariaLabel
- Typestring? / string?
- Behaviorhref renders a 44px-minimum link. String labels supply its default accessible name.
- Inputcollapse / threshold
- Typeboolean = true / number = 32
- BehaviorScroll-compaction switch and pixel threshold. Set collapse=false for a static lockup.
- InputexpandedWidth
- Typestring = "9rem"
- BehaviorLayout width of the open label; it snaps rather than transitions.
- InputlabelClassName / className
- Typestring? / string?
- BehaviorMerged last onto the label clip and root respectively.
Pagination
Numbered pagination in small mono type: an optional “21–40 of 142” range readout at left, then Prev, numbered pages with … gaps, and Next. The current page carries a full-ink underline, medium weight, and aria-current; Prev disables at the first page, Next at the last. The demo below is wired to local page state and includes the one-page readout state.
Live specimen
Rendered output
API reference
Inputs, types, and behavior
- Inputpage / onPageChange
- Typenumber / (page) => void
- Behavior1-based current page and its change handler.
- InputpageCount
- Typenumber
- BehaviorTotal number of pages.
- Inputtotal / pageSize
- Typenumber?
- BehaviorGiven both, renders the "21–40 of 142" range readout with tabular figures.
- InputsiblingCount
- Typenumber
- BehaviorPage numbers shown either side of the current page. Default 1.
- InputclassName
- Typestring?
- BehaviorMerged onto the nav.
- Inputlabel
- Typestring = "Pagination"
- BehaviorAccessible landmark name; use a unique label when multiple pagers share a page.
Command palette
A ⌘K command menu built on Modal’s dialog shell. The input implements the ARIA combobox pattern over a listbox. The query filters case-insensitively across string labels and every command’s keywords; results group under mono headers, arrows move through the flat list with wrap, and Enter runs the highlighted command and closes the menu. Tab remains contained in the search field while options use aria-activedescendant; Modal restores the opener on close. The useCommandKey hook binds ⌘K / Ctrl+K globally — press it now to try it.
Live specimen
Rendered output
Nothing run yet
const [open, setOpen] = useState(false);
useCommandKey(() => setOpen((v) => !v));
<CommandPalette
open={open}
onOpenChange={setOpen}
commands={[
{ id: "new", label: "New invoice", group: "Actions",
icon: <Icon as={Plus} />, shortcut: ["⌘", "N"], onSelect: create },
]}
/>API reference
Inputs, types, and behavior
- Inputopen / onOpenChange
- Typeboolean / (open) => void
- BehaviorControlled, via Modal — scroll lock, Escape, focus capture/restore; the palette contains Tab focus.
- Inputcommands
- TypeCommand[]
- Behavior{ id, label, group?, icon?, shortcut?, keywords?, onSelect }. Only string labels + keywords are searched.
- Inputplaceholder / label / emptyText
- Typestring
- BehaviorInput placeholder, dialog/listbox name, and the zero-results copy ("No results").
- InputclassName
- Typestring?
- BehaviorMerged onto the Modal panel.
- InputuseCommandKey(onToggle)
- Typehook
- BehaviorBinds ⌘K / Ctrl+K globally and preventDefaults the browser default; reads the latest callback through a ref.