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 a bar at the start edge, distinct from hover and press highlights. Built on Base UI's Navigation Menu.

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

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

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.2.13 ourostack/ouro-ui/navigation-menu-tests#v1.2.13
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, or else the longest link above it, gets aria-current="page", in the row or in a panel.
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;
      }
  );