Navigation

Navbar

The bar across the top of an app: your brand, the main links and a few controls, with the current page marked aria-current="page" and a bar at the start edge, distinct from hover and press highlights. A group of links ({ label, links }) opens a panel, as in NavigationMenu. Whenever the links do not fit, below 640px and on a narrow tablet, they collapse behind a menu button into a Sheet that slides in from the side, with each group under its name; the brand and actions stay in the bar. Links render through OuroProvider's linkComponent, so your router navigates.

In Spoonjoy

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

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

In its source: app/root.tsx

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/navbar#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/navbar

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/navbar#v1.2.13 ourostack/ouro-ui/navbar-tests#v1.2.13
import { Navbar } from "@/components/ouro/navbar/navbar";

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

Example

<Navbar
  label="Main"
  brand={<Link href="/">Spoonjoy</Link>}
  items={[
    { label: "Recipes", href: "/recipes" },
    { label: "Cookbooks", href: "/cookbooks" },
  ]}
  currentPath={pathname}
  actions={<Button href="/recipes/new">New recipe</Button>}
/>

Usage

The phone menu opens once JavaScript has loaded (after hydration); the links are in the server HTML from 640px up.

Use NavigationMenu alone for a row of panels inside a page, and Tabs for views of one page. StackedLayout puts a Navbar above the page.

Use instead when

  • Use Sidebar instead for more than a handful of destinations, or sections of them.

Props

Type: NavbarProps.

actions
Type: ReactNode
Controls at the end of the bar, at every width: a search button, an account Menu.
brand
Type: ReactNode
Your logo or product name, at the start of the bar. Link it to the home page.
className
Type: string
Layout only: margins, position (sticky top-0). Appearance classes warn in development.
closeLabel
Type: string
Default: "Close"
The label of that sheet's close button.
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 (/recipes for /recipes/12), gets aria-current="page" and the current look.
items (required)
Type: readonly NavbarItem[]
The links, in order, and groups of links ({ label, links }), which open a panel in the bar. When they do not fit, as on a phone, they move into a sheet.
label (required)
Type: string
Names the navigation for screen readers ("Main"). It is not shown.
menuLabel
Type: string
Default: "Menu"
The name of the button that opens the links below 640px, and the title of the sheet it opens.
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 bar is items, brand and actions.
  • links: Not a prop. Ouro UI uses items: <Navbar label="Main" items={[{ label: "Recipes", href: "/recipes" }]} />.
  • logo: Not a prop. Ouro UI uses brand: <Navbar brand={<Logo />}>.
  • pathname: Not a prop. Ouro UI uses currentPath: <Navbar 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

A group of links in a Navbar: a button that opens a panel in the bar, and a labelled group in the sheet.

interface NavbarGroup {
  /** The group's name, the button's text: "Recipes". */
  label: string;
  /**
   * The group's links, each with an optional `description`.
   */
  links: readonly NavbarLink[];
  /** Not with `links`. To link the section's own page, make it the group's first link. */
  href?: never;
}

One item of a Navbar: a link (href), or a group of links (links).

type NavbarItem = NavbarLink | NavbarGroup;

One link in a Navbar: in the bar, or in a group's panel.

interface NavbarLink {
  /** The link's text: "Recipes". */
  label: string;
  /** Where it goes. Internal paths go through `OuroProvider`'s `linkComponent` (your router's link). */
  href: string;
  /**
   * In a group: a line under the label in the bar's panel, "Everything you have saved."
   */
  description?: string | undefined;
  /**
   * Marks this link as the current page (`aria-current="page"`), when the Navbar's
   * `currentPath` cannot tell: a link whose page lives at another path.
   */
  current?: boolean | undefined;
  /** Not with `href`. An item is a link (`href`) or a group of links (`links`). */
  links?: never;
}