Skip to content

Navigation Menu

Organize site navigation into panels of related links.

import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuPositioner, NavigationMenuTrigger } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use it for a site's main navigation when destinations group into a few sections worth previewing, like products, solutions and resources. Don't use it for actions or options, which belong in a Dropdown Menu, or for an app's own navigation, where a Sidebar or Tabs keep every destination in view. On narrow screens, move the links into a Sheet instead.

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from '@youj/react'

export function SiteNav() {
  return (
    <NavigationMenu aria-label="Main">
      <NavigationMenuList>
        <NavigationMenuItem value="products">
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-[520px] grid-cols-2 gap-0.5">
              <li>
                <NavigationMenuLink href="/deployments">Deployments</NavigationMenuLink>
              </li>
              <li>
                <NavigationMenuLink href="/analytics">Analytics</NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink href="/pricing" className={navigationMenuTriggerStyle()}>
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Each NavigationMenuItem holds either a trigger and its content, or a plain link. Give a plain link navigationMenuTriggerStyle() so it matches the triggers beside it. Set a width on each panel's content: the popup measures it, and morphs between panel sizes as you move from trigger to trigger. Add closeOnClick to links that should close the menu when followed.

navigationMenuTriggerStyle() comes from a client module, so a header that calls it is a Client Component: start the file with 'use client'. In Next.js, render the framework's link through render so navigation stays client-side and prefetches:

import Link from 'next/link'

<NavigationMenuLink render={<Link href="/pricing" />} className={navigationMenuTriggerStyle()}>
  Pricing
</NavigationMenuLink>

A header with a mobile menu

Show the menu from md up. Below it, a Sheet opens from a menu button, holding the same links. Size the Sheet to its content with a width class (w-64 here, which the Sheet's default width yields to):

'use client'

import Link from 'next/link'
import {
  Button,
  NavigationMenu,
  NavigationMenuLink,
  NavigationMenuItem,
  NavigationMenuList,
  Sheet,
  SheetClose,
  SheetContent,
  SheetTitle,
  SheetTrigger,
  navigationMenuTriggerStyle,
} from '@youj/react'

const links = [
  { href: '/features', label: 'Features' },
  { href: '/pricing', label: 'Pricing' },
  { href: '/about', label: 'About' },
]

export function SiteHeader() {
  return (
    <header className="flex items-center justify-between gap-4 border-b border-line px-4 py-3">
      <Link href="/" className="font-semibold">
        Acme
      </Link>

      <NavigationMenu aria-label="Main" className="hidden md:flex">
        <NavigationMenuList>
          {links.map((link) => (
            <NavigationMenuItem key={link.href}>
              <NavigationMenuLink render={<Link href={link.href} />} className={navigationMenuTriggerStyle()}>
                {link.label}
              </NavigationMenuLink>
            </NavigationMenuItem>
          ))}
        </NavigationMenuList>
      </NavigationMenu>

      <Sheet>
        <SheetTrigger render={<Button variant="outline" className="md:hidden" />}>Menu</SheetTrigger>
        <SheetContent className="w-64">
          <SheetTitle>Menu</SheetTitle>
          <nav aria-label="Mobile" className="flex flex-col gap-1 p-4">
            {links.map((link) => (
              <SheetClose key={link.href} nativeButton={false} render={<Link href={link.href} />} className="rounded-md px-3 py-2 text-ui hover:bg-hover">
                {link.label}
              </SheetClose>
            ))}
          </nav>
        </SheetContent>
      </Sheet>
    </header>
  )
}

Reference

PropsEvery part passes its props through to the Base UI part of the same…

Props

Every part passes its props through to the Base UI part of the same name. NavigationMenu also takes the popup's placement: align, alignOffset, side and sideOffset. The ones you'll use most:

PartPropTypeDefault
NavigationMenualign'start' | 'center' | 'end''start'
NavigationMenualignOffset, side, sideOffsetnumber, 'top' | 'bottom' | …, number0, 'bottom', 8
NavigationMenuvalue, defaultValue, onValueChangethe open item's value–
NavigationMenudelaynumber, ms before opening on hover50
NavigationMenucloseDelaynumber, ms before closing50
NavigationMenuItemvalueany–
NavigationMenuTriggerdisabledbooleanfalse
NavigationMenuLinkactivebooleanfalse
NavigationMenuLinkcloseOnClickbooleanfalse
NavigationMenuContentkeepMountedbooleanfalse

NavigationMenu adds no props of its own. They pass through to NavigationMenuPrimitive.Root.Props, Pick<NavigationMenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'collisionPadding'>.

MotionHover pill, popup in, popup out… · reduced motion

Motion

Motion tokens
MomentTokens
Hover pillglides between triggers and links with the pointer and keyboard focus
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Popup inscales from 0.96 and fades, out of the trigger
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Popup out
duration.instantease.exit

80ms · cubic-bezier(0.4, 0, 1, 1)

80mscubic-bezier(0.4, 0, 1, 1)
Popup between triggersslides to the next trigger and morphs to its panel size
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Panels trade places24px drift and fade, away from the way you moved
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Chevronturns over while its panel is open
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Trigger color
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

The popup and its panels appear, resize and swap at once. The pill jumps and the chevron flips.

In right-to-left layouts the pill, the popup's slide and the panels' drift all mirror.

Reduced motion

The menu works the same, without movement: panels appear and swap in place, the popup takes its new size at once, the pill jumps to the item under the pointer or focus, and the chevron flips instead of turning.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • NavigationMenu is a nav landmark. Name it with aria-label ("Main"), and give each navigation landmark on a page a different name.
  • Triggers are buttons with aria-expanded. Their chevron is decorative.
  • The current page's link carries aria-current="page" when you set active.
  • The hover pill is hidden from assistive technology.
KeyAction
TabMove between triggers and links, and into an open panel.
Enter / Space / ↓Open the focused trigger's panel.
EscapeClose the panel and return focus to its trigger.