Skip to content
VVVER

Design System / 07.3

Navigation

Patterns for moving through pages, switching views, opening responsive menus, and finding commands.

Component reference

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.

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.

Component reference

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.

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.

Component reference

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.

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.

Component reference

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.

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.

Component reference

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.

Component reference

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.

Component reference

Adaptive navigation

NavBar shows inline links on md and larger, and folds them behind a MENU button below md. The button opens a full-screen overlay that rises from the bottom, revealing large stacked links one at a time in a staggered animation — the current page is marked via each link’s active flag. It traps focus, locks scroll, closes on Escape, link/CTA selection, or a resize to desktop, restores the trigger, sets inertwhile closed, and pads the safe area.

Live specimen

Rendered output

Resize under 768px — the links fold behind MENU into a full-screen overlay; the active link is marked by weight and a square/dot marker.

tsx
<NavBar
  brand={<Logo />}
  links={[
    { href: "/work", label: "Work", active: true },
    { href: "/studio", label: "Studio" },
    { href: "/contact", label: "Contact" },
  ]}
  cta={
    <Button
      className="border-current/15 text-current focus-visible:outline-current"
      href="/sign-in"
      size="sm"
    >
      Sign in
    </Button>
  }
/>

API reference

Inputs, types, and behavior

Inputlinks
Type{ href, label, active?, external? }[]
BehaviorRequired. active sets aria-current and the square marker; external adds a new-tab target and safe rel.
Inputbrand / cta
TypeReactNode?
BehaviorRendered on paper and in the ink curtain. Both must inherit currentColor; className merges let Button use border-current/text-current.
InputmenuLabel / closeLabel
Typestring = "Menu" / "Close"
BehaviorVisible control copy; menuLabel also names the dialog.
Inputlabel
Typestring = "Primary"
BehaviorAccessible name for the navigation landmark.
InputclassName
Typestring?
BehaviorMerged last onto the nav root.

Component reference

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

CATALOGCATALOG Reference

First: collapses past 32px, then re-expands on hover or focus. Second: static via collapse=false, with a non-string label and explicit ariaLabel.

tsx
<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.

Component reference

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.

Component reference

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

or press K

Nothing run yet

tsx
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.