Skip to main content

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 the link of the section it is in aria-current="true", both underlined in the primary colour as in FamilyBar, 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.

On this site

This site's own pages use Navbar 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

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

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.3.10 ourostack/ouro-ui/navbar-tests#v1.3.10
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. A tap on the menu button before then does nothing unless your app keeps it: this site's head script keeps a tap on any control React has not yet hydrated, shows the control pressed, and presses it again once it has (site/src/early-input.ts in the repository). Once the menu button shows, the sheet is built while the page is idle and kept, hidden, between opens, so a press only shows it.

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 gets aria-current="page" and the current look. On a deeper page, the longest link above it, the section (/recipes for /recipes/12), gets the same look and aria-current="true", so a screen reader hears one current page.
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.
menu
Type: ReactNode
What the sheet behind the menu button holds in place of the items links, when a phone needs more than the bar, such as a section's own pages under its name, or a filter over a long list. It replaces them, so list the links they held too. It sits in the sheet's nav; mark the current page yourself (aria-current="page"). Choosing a link in it closes the sheet. Once the page has been idle it mounts, hidden, ahead of the first press (a press before then mounts it as it opens), and it mounts afresh the same way after each close, so each open starts with the state it first had.
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;
}