Skip to main content
Graphite UI

Navigation Menu

A list of links to a site’s sections, laid out in a row or a column, with at most one nested level. It is the list only: a header bar, an action rail or a collapsible side panel around it is site chrome.

Contract 2.0.1No kit page

Live preview

Rendered by the component itself from the same generated tokens as the rest of the site.

Installation

Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.

npx shadcn@latest add https://www.graphite-ui.com/r/navigation-menu.json
import { NavigationMenu } from '@/components/ui/navigation-menu'
import type { NavItem, NavChild } from '@/components/ui/navigation-menu'

Anatomy

Top-level items, and nested items under any of them. The type stops there: a nested item has no items of its own, so a third level cannot be written.

  1. Top-level itemsRequired
  2. Nested items per top-level itemOptional

Variants

Orientation is the one prop. Horizontal wraps onto a new row when it runs out of room rather than scrolling; vertical is what a sidebar uses. Nested items render inline, open, in both.

Orientation: Horizontal
Orientation: Vertical
Vertical, with nested items

States

Focus is forced on the first link with the declarations :focus-visible carries. There is no Hover row because links have no hover style. Current is data, not a pseudo-class: the caller sets it on one item.

API reference

Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.

Navigation Menu props
PropTypeDefaultNotes
orientation'horizontal' | 'vertical'—

Design tokens

Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.

  • primaryActive/current-page indicator.
  • on-surfaceDefault items.
  • spacingContained list padding and gaps between levels.
  • radiusCorner on each item.

Usage

The contract's prohibitions, written as the choices you will actually face.

Do

  • Mark one item current per menu, and let the inset bar show it. It is a shape as well as a colour.
  • Use horizontal where every item fits on one row, and vertical for a short flat list. A nested sidebar is a Tree view.
  • Pass a distinct label to each menu when a page has more than one.
  • Keep nested items to the pages of their parent section, and few enough to leave the list open.

Don’t

  • Nest a third level. The contract says it becomes its own page, and the type cannot express it.
  • Fold a header bar, an action rail or a collapsible panel into it. That is site chrome, and it lives outside components/ui.
  • Replace the inset bar with a colour change alone. Colour never carries meaning on its own here.
  • Expect nested items to open as a flyout. That waits on the Wave 5 overlay surface; today they sit inline.

Accessibility

What the component does for you, and what it leaves to you.

Landmark
A nav landmark named by label, which defaults to “Main”. Give each menu on a page its own name.
Current page
The current item carries aria-current="page", and a 2px inset bar in primary as well as the colour change.
Keyboard
Plain links in lists. Tab moves through them in order, nested items included; there is no arrow-key handling to learn.
Focus
A 2px --graphite-focus outline inside the link, on :focus-visible only.
Structure
Nested items are a list inside their parent’s list item, so the hierarchy a screen reader announces is the one on screen.