Przejdź do treści

Navigation Menu

Główna nawigacja witryny z rozbudowanymi, animowanymi panelami — dla stron marketingowych i dokumentacji, w których każda sekcja potrzebuje czegoś więcej niż jednego linku.

Kod źródłowyBase UIRejestr
import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const components = [
  {
    title: "Button",
    href: "/pl/docs/components/button",
    description: "Wywołuje akcję. Najważniejszy element interaktywny.",
  },
  {
    title: "Dialog",
    href: "/pl/docs/components/dialog",
    description: "Skupione zadanie nad stroną, z jasną drogą wyjścia.",
  },
  {
    title: "Tabs",
    href: "/pl/docs/components/tabs",
    description: "Przełączanie powiązanych widoków bez opuszczania strony.",
  },
  {
    title: "Command",
    href: "/pl/docs/components/command",
    description: "Szybkie wyszukiwanie i akcje, przede wszystkim z klawiatury.",
  },
  {
    title: "Select",
    href: "/pl/docs/components/select",
    description: "Wybór jednej opcji z listy zdefiniowanych wartości.",
  },
  {
    title: "Toast",
    href: "/pl/docs/components/toast",
    description: "Krótka, nieblokująca informacja zwrotna o operacji.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Pierwsze kroki</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-120 grid-cols-[11rem_1fr] gap-1.5">
              <li className="row-span-3">
                <NavigationMenuLink
                  render={<Link href="/pl/docs" />}
                  className="flex h-full flex-col items-start justify-end gap-1.5 rounded-lg bg-muted p-4 transition-colors duration-fast hover:bg-gray-4"
                >
                  <span className="text-heading-sm text-foreground">prfct</span>
                  <span className="text-body-xs text-muted-foreground">
                    Interfejsy dopięte na ostatni guzik.
                  </span>
                </NavigationMenuLink>
              </li>
              {[
                {
                  title: "Wprowadzenie",
                  href: "/pl/docs",
                  description: "Czym jest prfct i jak jest zorganizowany.",
                },
                {
                  title: "Instalacja",
                  href: "/pl/docs/installation",
                  description: "Dodaj prfct do projektu Next.js.",
                },
                {
                  title: "Fundamenty",
                  href: "/pl/docs/foundations/color",
                  description: "Kolor, typografia, odstępy, ruch.",
                },
              ].map((item) => (
                <li key={item.title}>
                  <NavigationMenuLink
                    render={<Link href={item.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {item.title}
                    </span>
                    <span className="text-body-xs text-muted-foreground">
                      {item.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Komponenty</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-136 grid-cols-2 gap-1">
              {components.map((component) => (
                <li key={component.title}>
                  <NavigationMenuLink
                    render={<Link href={component.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {component.title}
                    </span>
                    <span className="line-clamp-2 text-body-xs text-muted-foreground">
                      {component.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink
            render={<Link href="/pl/themes" />}
            className={navigationMenuTriggerStyle()}
          >
            Motywy
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/navigation-menu.json

Użycie

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink render={<Link href="/analytics" />}>
          Analytics
        </NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink
        render={<Link href="/pricing" />}
        className={navigationMenuTriggerStyle()}
      >
        Pricing
      </NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>

Wszystkie panele dzielą jeden popup. Gdy użytkownik przechodzi od jednego wyzwalacza do kolejnego, popup płynnie dopasowuje się do rozmiaru nowego panelu, a treść wsuwa się z kierunku ruchu — to wyraźna wskazówka przestrzenna, że przegląda elementy tego samego poziomu, a nie otwiera czegoś nowego.

Przykłady

Łącz wyzwalacze otwierające panele ze zwykłymi linkami ostylowanymi przez navigationMenuTriggerStyle() — dzięki temu każdy element paska wygląda i zachowuje się spójnie.

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const components = [
  {
    title: "Button",
    href: "/pl/docs/components/button",
    description: "Wywołuje akcję. Najważniejszy element interaktywny.",
  },
  {
    title: "Dialog",
    href: "/pl/docs/components/dialog",
    description: "Skupione zadanie nad stroną, z jasną drogą wyjścia.",
  },
  {
    title: "Tabs",
    href: "/pl/docs/components/tabs",
    description: "Przełączanie powiązanych widoków bez opuszczania strony.",
  },
  {
    title: "Command",
    href: "/pl/docs/components/command",
    description: "Szybkie wyszukiwanie i akcje, przede wszystkim z klawiatury.",
  },
  {
    title: "Select",
    href: "/pl/docs/components/select",
    description: "Wybór jednej opcji z listy zdefiniowanych wartości.",
  },
  {
    title: "Toast",
    href: "/pl/docs/components/toast",
    description: "Krótka, nieblokująca informacja zwrotna o operacji.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Pierwsze kroki</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-120 grid-cols-[11rem_1fr] gap-1.5">
              <li className="row-span-3">
                <NavigationMenuLink
                  render={<Link href="/pl/docs" />}
                  className="flex h-full flex-col items-start justify-end gap-1.5 rounded-lg bg-muted p-4 transition-colors duration-fast hover:bg-gray-4"
                >
                  <span className="text-heading-sm text-foreground">prfct</span>
                  <span className="text-body-xs text-muted-foreground">
                    Interfejsy dopięte na ostatni guzik.
                  </span>
                </NavigationMenuLink>
              </li>
              {[
                {
                  title: "Wprowadzenie",
                  href: "/pl/docs",
                  description: "Czym jest prfct i jak jest zorganizowany.",
                },
                {
                  title: "Instalacja",
                  href: "/pl/docs/installation",
                  description: "Dodaj prfct do projektu Next.js.",
                },
                {
                  title: "Fundamenty",
                  href: "/pl/docs/foundations/color",
                  description: "Kolor, typografia, odstępy, ruch.",
                },
              ].map((item) => (
                <li key={item.title}>
                  <NavigationMenuLink
                    render={<Link href={item.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {item.title}
                    </span>
                    <span className="text-body-xs text-muted-foreground">
                      {item.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Komponenty</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-136 grid-cols-2 gap-1">
              {components.map((component) => (
                <li key={component.title}>
                  <NavigationMenuLink
                    render={<Link href={component.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {component.title}
                    </span>
                    <span className="line-clamp-2 text-body-xs text-muted-foreground">
                      {component.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink
            render={<Link href="/pl/themes" />}
            className={navigationMenuTriggerStyle()}
          >
            Motywy
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Dwukolumnowa lista miejsc docelowych, każde z ikoną i jednowierszowym opisem. To opisy robią tu najwięcej: pozwalają wybrać bez przeklikiwania się przez kolejne strony.

import {
  ChartColumnIcon,
  DatabaseIcon,
  GlobeIcon,
  ShieldCheckIcon,
  WorkflowIcon,
  ZapIcon,
} from "lucide-react"
import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const products = [
  {
    title: "Analityka",
    description: "Śledź wykorzystanie w czasie rzeczywistym.",
    icon: ChartColumnIcon,
  },
  {
    title: "Automatyzacje",
    description: "Przepływy pracy, które działają same.",
    icon: WorkflowIcon,
  },
  {
    title: "Sieć brzegowa",
    description: "Obsługuj każdy region w mniej niż 50 ms.",
    icon: GlobeIcon,
  },
  {
    title: "Magazyn danych",
    description: "Trwały magazyn obiektów z wersjonowaniem.",
    icon: DatabaseIcon,
  },
  {
    title: "Bezpieczeństwo",
    description: "SSO, logi audytowe i SCIM.",
    icon: ShieldCheckIcon,
  },
  {
    title: "Funkcje",
    description: "Uruchamiaj kod blisko swoich użytkowników.",
    icon: ZapIcon,
  },
]

export default function NavigationMenuIconList() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Produkty</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-144 grid-cols-2 gap-1">
              {products.map(({ title, description, icon: Icon }) => (
                <li key={title}>
                  <NavigationMenuLink
                    render={<Link href="/pl/docs/components/navigation-menu" />}
                    className="items-start gap-3 p-3"
                  >
                    <span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-card text-muted-foreground shadow-raised">
                      <Icon className="size-4" />
                    </span>
                    <span className="flex flex-col gap-0.5">
                      <span className="font-medium text-foreground">
                        {title}
                      </span>
                      <span className="text-body-xs text-muted-foreground">
                        {description}
                      </span>
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Zarezerwuj boczną kolumnę panelu na jedną aktualną pozycję — premierę, poradnik, wydarzenie. Jedno wyróżnienie na panel; dwa zaczną ze sobą konkurować.

import {
  ArrowRightIcon,
  BookOpenIcon,
  NewspaperIcon,
  PlayCircleIcon,
} from "lucide-react"
import Link from "next/link"

import { Badge } from "@/components/ui/badge"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const resources = [
  {
    title: "Dokumentacja",
    description: "Przewodniki i dokumentacja API.",
    icon: BookOpenIcon,
  },
  {
    title: "Lista zmian",
    description: "Co wydaliśmy w tym tygodniu.",
    icon: NewspaperIcon,
  },
  {
    title: "Samouczki",
    description: "Lekcje wideo krok po kroku.",
    icon: PlayCircleIcon,
  },
]

export default function NavigationMenuFeatured() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Zasoby</NavigationMenuTrigger>
          <NavigationMenuContent>
            <div className="grid w-128 grid-cols-[1fr_13rem] gap-1.5">
              <ul className="flex flex-col gap-1">
                {resources.map(({ title, description, icon: Icon }) => (
                  <li key={title}>
                    <NavigationMenuLink
                      render={<Link href="/pl/docs" />}
                      className="items-start gap-3 p-3"
                    >
                      <Icon className="mt-0.5 text-muted-foreground" />
                      <span className="flex flex-col gap-0.5">
                        <span className="font-medium text-foreground">
                          {title}
                        </span>
                        <span className="text-body-xs text-muted-foreground">
                          {description}
                        </span>
                      </span>
                    </NavigationMenuLink>
                  </li>
                ))}
              </ul>
              <NavigationMenuLink
                render={<Link href="/pl/docs/installation" />}
                className="flex-col items-start justify-between gap-6 rounded-lg border bg-surface p-4 hover:bg-accent"
              >
                <Badge variant="brand" size="sm">
                  Nowość
                </Badge>
                <span className="flex flex-col gap-1">
                  <span className="font-medium text-foreground">
                    Zobacz, co nowego
                  </span>
                  <span className="text-body-xs text-muted-foreground">
                    Tokeny, ponad 50 komponentów i kreator motywów.
                  </span>
                  <span className="mt-2 inline-flex items-center gap-1 text-label-sm text-link">
                    Przeczytaj ogłoszenie
                    <ArrowRightIcon className="size-3.5" />
                  </span>
                </span>
              </NavigationMenuLink>
            </div>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Gdy nie ma paneli, menu jest ostylowaną listą linków. Każdy link pozostaje osobnym przystankiem klawisza Tab, a między linkami można też przechodzić strzałkami. Bieżącą sekcję oznacz propem active — ustawia on aria-current="page" i styl aktywnego elementu.

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const links = [
  { title: "Przegląd", href: "/pl/docs" },
  { title: "Komponenty", href: "/pl/docs/components" },
  { title: "Motywy", href: "/pl/themes" },
  { title: "Przykłady", href: "/pl/examples" },
]

export default function NavigationMenuLinksOnly() {
  const current = "/docs/components"

  return (
    <NavigationMenu>
      <NavigationMenuList className="gap-1">
        {links.map((link) => (
          <NavigationMenuItem key={link.href}>
            <NavigationMenuLink
              active={link.href === current}
              render={<Link href={link.href} />}
              className={navigationMenuTriggerStyle({
                className: "data-active:bg-accent data-active:text-foreground",
              })}
            >
              {link.title}
            </NavigationMenuLink>
          </NavigationMenuItem>
        ))}
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Wytyczne

Kiedy używać

  • Jako główna nawigacja stron marketingowych i dokumentacji z kilkoma sekcjami najwyższego poziomu.
  • Gdy sekcja potrzebuje drogowskazów — opisów, grupowania albo wyróżnionej pozycji — zanim użytkownik zdecyduje, dokąd przejść.

Kiedy nie używać

  • Do nawigacji w aplikacji z wieloma miejscami docelowymi lub głęboką hierarchią — użyj Sidebar.
  • Do poleceń i akcji — użyj Dropdown Menu lub Menubar. Menu nawigacyjne zawiera wyłącznie linki.
  • Na małych ekranach — poniżej md zwiń nawigację do prostej listy w panelu Sheet.

Zawartość panelu

Trzymaj się pięciu–siedmiu pozycji najwyższego poziomu. Każdy link w panelu dostaje krótki tytuł i najlepiej jednowierszowy opis w formie zwykłego zdania — z tą samą interpunkcją w całym panelu. Długie panele podziel na kolumny powiązanych linków i nie chowaj niczego ważnego wyłącznie w panelu — to samo miejsce powinno być osiągalne także z samej strony albo ze stopki.

AnalitykaŚledź wykorzystanie w czasie rzeczywistym
AutomatyzacjeProcesy, które działają same
DobrzeTytuły, które użytkownicy rozpoznają, i opisy, które pomagają wybrać.
Pulse
Flow Engine X
Nimbus
ŹleMarketingowe nazwy bez kontekstu zmuszają do klikania, żeby sprawdzić, co się za nimi kryje.

Najechanie i kliknięcie

Panele otwierają się po kliknięciu oraz po najechaniu, z krótkim opóźnieniem delay (50 ms). Popup i wyzwalacz łączy niewidoczny mostek, więc kursor może przejść po skosie do panelu, nie zamykając go. Nigdy nie umieszczaj celu nawigacji na samym wyzwalaczu — wyzwalacz tylko otwiera swój panel; strona przeglądowa sekcji należy do panelu jako jego pierwszy link.

Dostępność

Navigation Menu renderuje element <nav> z listą pozycji. Wyzwalacze to przyciski z aria-expanded; panele to rozwijana treść, a nie menu — do linków w środku przechodzi się klawiszem Tab, tak samo jak na stronie.

KlawiszDziałanie
Tab
Przechodzi między wyzwalaczami i linkami, a także do linków otwartego panelu.
←→
Przenosi fokus między wyzwalaczami i linkami najwyższego poziomu.
EnterSpacja
Otwiera lub zamyka panel wyzwalacza, na którym jest fokus. Na linku tylko Enter przechodzi pod jego adres, jak przy każdym linku.
↓
Na wyzwalaczu otwiera jego panel i przenosi do niego fokus.
Esc
Zamyka panel i przywraca fokus na jego wyzwalacz.
  • Bieżąca strona. Przekaż active do linku bieżącej strony lub sekcji — technologie wspomagające dostaną tę informację jako aria-current="page".
  • Nazwij każdą nawigację. Jeśli strona ma więcej niż jeden <nav>, nadaj każdemu aria-label (Główna, Stopka), aby użytkownicy czytników ekranu mogli je rozróżnić.

Dokumentacja API

Główny element <nav>. Przyjmuje wszystkie propsy NavigationMenu.Root z Base UI. Po swoich elementach potomnych renderuje NavigationMenuPositioner: portal, pozycjoner, popup i viewport wspólne dla wszystkich paneli. Nie umieszczasz go samodzielnie; jest eksportowany na potrzeby budowy własnego komponentu głównego i przyjmuje side ("bottom"), sideOffset (8), align ("start") oraz alignOffset (0).

PropTypDomyślnie
value

Kontrolowana wartość otwartej pozycji. Wartość inna niż null oznacza, że panel jest otwarty.

anynull
defaultValue

Pozycja otwarta na starcie, gdy menu jest niekontrolowane.

anynull
onValueChange

Wywoływana, gdy zmienia się otwarta pozycja.

(value: any, details) => voidBrak wartości domyślnej
delay

Opóźnienie otwarcia panelu po najechaniu, w ms.

number50
closeDelay

Opóźnienie zamknięcia po opuszczeniu menu przez kursor, w ms.

number50
orientation

Układ i kierunek nawigacji strzałkami.

"horizontal" | "vertical""horizontal"
align

Wyrównanie wspólnego popupu względem aktywnego wyzwalacza.

"start" | "center" | "end""start"
PropTypDomyślnie
value

Identyfikuje pozycję w trybie kontrolowanym. Jeśli go pominiesz, zostanie wygenerowany.

anyBrak wartości domyślnej

Otwiera panel swojej pozycji. Renderuje strzałkę, która obraca się, gdy panel jest otwarty. Przyjmuje disabled.

Panel pozycji. Jego rozmiar ustal, nadając szerokość pierwszemu elementowi potomnemu; popup animuje przejścia między rozmiarami paneli.

PropTypDomyślnie
keepMounted

Zachowuje treść zamkniętego panelu w DOM, aby roboty wyszukiwarek mogły indeksować jego linki.

booleanfalse
PropTypDomyślnie
active

Oznacza bieżącą stronę: ustawia aria-current i styl aktywnego elementu.

booleanfalse
closeOnClick

Zamyka otwarty panel po kliknięciu linku.

booleanfalse
render

Renderuje link frameworka, np. <Link href="/pricing" />.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

Generator klas, dzięki któremu zwykły NavigationMenuLink wygląda dokładnie jak wyzwalacz. Przyjmuje { className }, aby rozszerzyć style.