Skip to main content

Navigation

NavigationMenu

A row of links where some open a panel of further links, each with a line of description: the navigation of a site with more pages than fit in a bar. Panels open on hover or click, move between each other without closing, and close on Escape, on an outside press and when a link is chosen. The current page is marked aria-current="page", and the link of the section it is in aria-current="true", both underlined in the primary colour, distinct from hover and press highlights. Built on Base UI's Navigation Menu.

On this site

This site's own pages use NavigationMenu too. Open one with X-ray pointing it out: The Nautilus's story · The home page

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

See it live, no sign-in needed: Home · A recipe · Recipes

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/navigation-menu#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/navigation-menu

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/navigation-menu#v1.3.10 ourostack/ouro-ui/navigation-menu-tests#v1.3.10
import { NavigationMenu } from "@/components/ouro/navigation-menu/navigation-menu";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<NavigationMenu
  label="Main"
  items={[
    {
      label: "Recipes",
      links: [
        { label: "Browse", href: "/recipes", description: "Everything you have saved." },
        { label: "Import", href: "/recipes/import", description: "Bring one in from a link." },
      ],
    },
    { label: "Pricing", href: "/pricing" },
  ]}
  currentPath={pathname}
/>

Usage

Use Menu for actions rather than pages, and Sidebar for a column of links.

Use instead when

  • Use Navbar instead when every item is a plain link; it also collapses into a drawer on a phone, which this row does not.

Props

Type: NavigationMenuProps.

className
Type: string
Layout only: margins, grid placement (ms-4). Appearance classes warn in development.
currentPath
Type: string
The path of the page being shown, from your router (useLocation().pathname, or usePathname() in Next.js). The link to that path gets aria-current="page", in the row or in a panel; else the longest link above it, the section the page is in, gets aria-current="true", so a screen reader hears one current page.
items (required)
Type: readonly NavigationMenuItem[]
The row's items, in order: links, and buttons that open a panel of links.
label (required)
Type: string
Names the navigation for screen readers ("Main"). It is not shown.
ref
Type: Ref<HTMLElement>
Reaches the rendered element.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • children: Not rendered. The menu is data: items={[{ label: "Recipes", links: [...] }, { label: "Pricing", href: "/pricing" }]}.
  • links: Not a prop. Ouro UI uses items; a panel's links go in an item's links.
  • orientation: Not a prop. The menu is a row; for a column of links use Sidebar.
  • pathname: Not a prop. Ouro UI uses currentPath: <NavigationMenu currentPath={pathname}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

One item of a NavigationMenu: a link (href), or a button that opens a panel (links, content).

type NavigationMenuItem = NavigationMenuLinkItem | NavigationMenuPanelItem;

One link inside a NavigationMenu panel.

interface NavigationMenuLink {
  /** The link's text: "Browse". */
  label: string;
  /** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
  href: string;
  /**
   * A line under the label that says what is there: "Everything you have saved."
   */
  description?: string | undefined;
  /** Marks this link as the current page, when the menu's `currentPath` cannot tell. */
  current?: boolean | undefined;
}

A NavigationMenu item that is a link to a page.

interface NavigationMenuLinkItem {
  /** The link's text: "Pricing". */
  label: string;
  /** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
  href: string;
  /** Marks this link as the current page, when the menu's `currentPath` cannot tell. */
  current?: boolean | undefined;
  /** Not with `href`. An item is a link (`href`) or a panel (`links`, `content`). */
  links?: never;
  /** Not with `href`. An item is a link (`href`) or a panel (`links`, `content`). */
  content?: never;
}

A NavigationMenu item that opens a panel: a list of links, with any content at its start, such as a featured Card beside a two-column grid of links. A panel needs one of the two.

type NavigationMenuPanelItem = NavigationMenuPanelBase &
  (
    | {
        /**
         * The links in the panel, each with an optional `description`.
         */
        links: readonly NavigationMenuLink[];
        /**
         * Anything else in the panel, before its links (above them on a phone): a featured
         * `Card`, an image, a line of text. A link in it closes the panel when it is followed.
         */
        content?: ReactNode;
      }
    | {
        links?: undefined;
        /**
         * The panel's content, when it has no list of links. A link in it closes the panel when it is followed.
         */
        content: ReactNode;
      }
  );