Przejdź do treści

Tabs

Przełącza powiązane widoki o wspólnym kontekście — pokazuje jeden panel naraz, bez opuszczania strony.

Kod źródłowyBase UIRejestr

Przegląd projektu

12 otwartych zgłoszeń, 3 pull requesty czekające na recenzję, ostatnie wdrożenie 18 minut temu.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsDemo() {
  return (
    <Tabs defaultValue="overview" className="w-full max-w-md">
      <TabsList>
        <TabsTrigger value="overview">Przegląd</TabsTrigger>
        <TabsTrigger value="activity">Aktywność</TabsTrigger>
        <TabsTrigger value="settings">Ustawienia</TabsTrigger>
      </TabsList>
      <TabsContent
        value="overview"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Przegląd projektu</p>
        <p className="mt-1 text-muted-foreground">
          12 otwartych zgłoszeń, 3 pull requesty czekające na recenzję, ostatnie wdrożenie 18 minut temu.
        </p>
      </TabsContent>
      <TabsContent
        value="activity"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Ostatnia aktywność</p>
        <p className="mt-1 text-muted-foreground">
          Grace scaliła „Popraw pierścienie fokusu”, a Alan otworzył dwa zgłoszenia.
        </p>
      </TabsContent>
      <TabsContent
        value="settings"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Ustawienia projektu</p>
        <p className="mt-1 text-muted-foreground">
          Zmień nazwę projektu, zarządzaj członkami i skonfiguruj wdrożenia.
        </p>
      </TabsContent>
    </Tabs>
  )
}

Anatomia

Przegląd projektu
  1. 1ListaPasek, na którym leżą zakładki: domyślnie kontrolka segmentowa, a z variant="line" — cienka linia.
  2. 2WyzwalaczZakładka. Krótka etykieta w formie rzeczownika, opcjonalnie z ikoną na początku.
  3. 3WskaźnikPojedynczy element, który przesuwa się do aktywnej zakładki.
  4. 4PanelTreść aktywnej zakładki. Przyjmuje fokus, więc osoby korzystające z klawiatury mogą do niej dotrzeć.

Instalacja

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

Użycie

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account settings.</TabsContent>
  <TabsContent value="password">Password settings.</TabsContent>
</Tabs>

Każdy TabsTrigger musi znajdować się wewnątrz TabsList, a każdy TabsContent tworzy parę z wyzwalaczem o tym samym value. Lista renderuje własny wskaźnik: jeden element, który płynnie przesuwa się do aktywnej zakładki według pozycji zmierzonej przez Base UI — zamiast tła, które każda zakładka osobno rozjaśnia i wygasza.

Przykłady

Wariant segmentowy

Domyślny wariant to kontrolka segmentowa — aktywna zakładka leży na wyniesionej „pigułce” wewnątrz stonowanego paska. Używaj go do kompaktowego przełączania widoków oraz do zakładek w kartach, na paskach narzędzi i w oknach dialogowych.

Przegląd projektu

12 otwartych zgłoszeń, 3 pull requesty czekające na recenzję, ostatnie wdrożenie 18 minut temu.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsDemo() {
  return (
    <Tabs defaultValue="overview" className="w-full max-w-md">
      <TabsList>
        <TabsTrigger value="overview">Przegląd</TabsTrigger>
        <TabsTrigger value="activity">Aktywność</TabsTrigger>
        <TabsTrigger value="settings">Ustawienia</TabsTrigger>
      </TabsList>
      <TabsContent
        value="overview"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Przegląd projektu</p>
        <p className="mt-1 text-muted-foreground">
          12 otwartych zgłoszeń, 3 pull requesty czekające na recenzję, ostatnie wdrożenie 18 minut temu.
        </p>
      </TabsContent>
      <TabsContent
        value="activity"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Ostatnia aktywność</p>
        <p className="mt-1 text-muted-foreground">
          Grace scaliła „Popraw pierścienie fokusu”, a Alan otworzył dwa zgłoszenia.
        </p>
      </TabsContent>
      <TabsContent
        value="settings"
        className="rounded-xl border bg-card p-5 shadow-raised"
      >
        <p className="font-medium text-foreground">Ustawienia projektu</p>
        <p className="mt-1 text-muted-foreground">
          Zmień nazwę projektu, zarządzaj członkami i skonfiguruj wdrożenia.
        </p>
      </TabsContent>
    </Tabs>
  )
}

Wariant liniowy

variant="line" rysuje cienką linię z przesuwającym się podkreśleniem. Używaj go do sekcji na poziomie strony, gdzie zakładki pełnią rolę drugiego paska nawigacji pod tytułem strony.

Ruch wzrósł o 12% względem poprzedniego tygodnia, głównie dzięki wyszukiwaniu organicznemu.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "overview",
    label: "Przegląd",
    body: "Ruch wzrósł o 12% względem poprzedniego tygodnia, głównie dzięki wyszukiwaniu organicznemu.",
  },
  {
    value: "analytics",
    label: "Analityka",
    body: "Mediana czasu trwania sesji to 4 min 12 s przy 18 420 sesjach.",
  },
  {
    value: "reports",
    label: "Raporty",
    body: "Trzy zaplanowane raporty wychodzą w każdy poniedziałek o 9:00.",
  },
  {
    value: "alerts",
    label: "Alerty",
    body: "Brak alertów w ciągu ostatnich 30 dni. Progi są w normie.",
  },
]

export default function TabsLine() {
  return (
    <Tabs defaultValue="overview" className="w-full max-w-lg">
      <TabsList variant="line">
        {tabs.map((tab) => (
          <TabsTrigger key={tab.value} value={tab.value}>
            {tab.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {tabs.map((tab) => (
        <TabsContent
          key={tab.value}
          value={tab.value}
          className="text-muted-foreground"
        >
          {tab.body}
        </TabsContent>
      ))}
    </Tabs>
  )
}

Układ pionowy

Ustaw orientation="vertical" na stronach ustawień i przy długich listach sekcji. Nawigacja strzałkami sama przełącza się wtedy na ↑ i ↓.

Ogólne

Nazwa przestrzeni roboczej, adres URL i język domyślny.

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const sections = [
  {
    value: "general",
    label: "Ogólne",
    title: "Ogólne",
    body: "Nazwa przestrzeni roboczej, adres URL i język domyślny.",
  },
  {
    value: "members",
    label: "Członkowie",
    title: "Członkowie",
    body: "Zapraszaj osoby i zarządzaj ich rolami.",
  },
  {
    value: "billing",
    label: "Płatności",
    title: "Płatności",
    body: "Plan, metoda płatności i faktury.",
  },
  {
    value: "security",
    label: "Bezpieczeństwo",
    title: "Bezpieczeństwo",
    body: "Logowanie jednokrotne (SSO), wymuszanie 2FA i sesje.",
  },
]

export default function TabsVertical() {
  return (
    <Tabs
      defaultValue="general"
      orientation="vertical"
      className="w-full max-w-lg gap-8"
    >
      <TabsList variant="line" className="w-40 shrink-0">
        {sections.map((section) => (
          <TabsTrigger key={section.value} value={section.value}>
            {section.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {sections.map((section) => (
        <TabsContent key={section.value} value={section.value}>
          <p className="text-heading-sm text-foreground">{section.title}</p>
          <p className="mt-1 text-muted-foreground">{section.body}</p>
        </TabsContent>
      ))}
    </Tabs>
  )
}

Z ikonami

Ikony ułatwiają przeglądanie zakładek, które odpowiadają układom lub typom treści. Zostaw etykietę tekstową — sama ikona rzadko jest jednoznaczna.

import { KanbanIcon, LayoutGridIcon, ListIcon } from "lucide-react"

import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsIcons() {
  return (
    <Tabs defaultValue="list">
      <TabsList aria-label="Układ">
        <TabsTrigger value="list">
          <ListIcon />
          Lista
        </TabsTrigger>
        <TabsTrigger value="grid">
          <LayoutGridIcon />
          Siatka
        </TabsTrigger>
        <TabsTrigger value="board">
          <KanbanIcon />
          Tablica
        </TabsTrigger>
      </TabsList>
    </Tabs>
  )
}

Wyłączona zakładka

Wyłącz zakładkę, gdy jej widok istnieje, ale jest niedostępny — i wyjaśnij dlaczego, krótką odznaką albo podpowiedzią. Wyłączonej zakładki nie da się aktywować, ale strzałki nadal przenoszą na nią fokus, dzięki czemu osoby korzystające z klawiatury i czytników ekranu mogą ją znaleźć i usłyszeć, że jest niedostępna.

Przeglądaj pliki, gałęzie i commity.
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

export default function TabsDisabled() {
  return (
    <Tabs defaultValue="code" className="w-full max-w-md">
      <TabsList variant="line">
        <TabsTrigger value="code">Kod</TabsTrigger>
        <TabsTrigger value="issues">Zgłoszenia</TabsTrigger>
        <TabsTrigger value="deployments" disabled>
          Wdrożenia
          <Badge variant="secondary" size="sm">
            Wkrótce
          </Badge>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="code" className="text-muted-foreground">
        Przeglądaj pliki, gałęzie i commity.
      </TabsContent>
      <TabsContent value="issues" className="text-muted-foreground">
        Śledź błędy i prośby o nowe funkcje.
      </TabsContent>
    </Tabs>
  )
}

Tryb kontrolowany

Kontroluj value, gdy widok zmienia coś innego niż same zakładki — tutaj przycisk Dalej w kreatorze krokowym.

Nazwij projekt i wybierz region.
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const steps = [
  {
    value: "details",
    label: "Szczegóły",
    body: "Nazwij projekt i wybierz region.",
  },
  {
    value: "team",
    label: "Zespół",
    body: "Zaproś osoby, z którymi będziesz pracować.",
  },
  {
    value: "review",
    label: "Podsumowanie",
    body: "Sprawdź, czy wszystko się zgadza, i utwórz projekt.",
  },
]

export default function TabsControlled() {
  const [value, setValue] = React.useState("details")
  const index = steps.findIndex((step) => step.value === value)

  return (
    <Tabs value={value} onValueChange={setValue} className="w-full max-w-md">
      <TabsList className="w-full">
        {steps.map((step, i) => (
          <TabsTrigger key={step.value} value={step.value}>
            <span className="font-mono text-xs text-muted-foreground">
              {i + 1}
            </span>
            {step.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {steps.map((step) => (
        <TabsContent
          key={step.value}
          value={step.value}
          className="text-muted-foreground"
        >
          {step.body}
        </TabsContent>
      ))}
      <div className="flex justify-between">
        <Button
          variant="outline"
          disabled={index === 0}
          onClick={() => setValue(steps[index - 1].value)}
        >
          Wstecz
        </Button>
        <Button
          disabled={index === steps.length - 1}
          onClick={() => setValue(steps[index + 1].value)}
        >
          Dalej
        </Button>
      </div>
    </Tabs>
  )
}

Wytyczne

Kiedy używać

  • Do porządkowania powiązanych treści o równej wadze, między którymi użytkownik się przełącza i których nie musi porównywać obok siebie.
  • Do dzielenia długich stron ustawień lub szczegółów na sekcje, które mają wspólny nagłówek i kontekst.

Kiedy nie używać

  • Do kolejnych kroków, które trzeba wykonać po kolei — użyj kreatora z wyraźnymi przyciskami Wstecz i Dalej albo jednego przewijanego formularza.
  • Do treści, które trzeba porównać — pokaż je obok siebie.
  • Do nawigacji między stronami — zakładki przełączają widoki w miejscu, a linki należą do Navigation Menu lub Sidebar.
  • Do zestawu dwóch–pięciu opcji, które ustawiają wartość — użyj Toggle Group.

Etykiety

Używaj krótkich, konkretnych rzeczowników — po jednym lub dwa słowa — z wielką literą tylko na początku. Etykieta musi opisywać swój panel, żeby dało się przewidzieć, co się w nim zobaczy: Członkowie, a nie Więcej. Nie łam etykiet zakładek na dwa wiersze; jeśli się nie mieszczą, zakładek jest za dużo.

DobrzeKrótkie, jednorodne etykiety, które zapowiadają zawartość.
ŹleDługie, niespójne etykiety utrudniają przeglądanie paska.

Wybór wariantu

Wybierz jeden wariant dla danej powierzchni i trzymaj się go. line należy do ramy strony, a domyślny wariant segmentowy — do wnętrza komponentów. Nigdy nie zagnieżdżaj jednego zestawu zakładek w panelu drugiego — powstają wtedy dwa konkurujące ze sobą miejsca, które odpowiadają na pytanie „gdzie jestem?”.

Stan i adresy URL

Gdy zakładki odpowiadają sekcjom strony, którymi ktoś może się podzielić albo do których może wrócić, zapisuj aktywną zakładkę w adresie URL (na przykład ?tab=billing) i na tej podstawie steruj value. Utrzymuj panele zamontowane przez keepMounted tylko wtedy, gdy ich stan musi przetrwać przełączanie.

Dostępność

Tabs implementuje wzorzec WAI-ARIA tabs: tablist złożony z elementów tab, z których każdy steruje jednym tabpanel. Lista zajmuje jedno miejsce w kolejności tabulacji, a fokus wędruje w jej obrębie (roving focus).

KlawiszDziałanie
Tab
Przenosi fokus na aktywną zakładkę, a potem do aktywnego panelu.
←→
W zakładkach poziomych przenosi fokus na poprzednią lub następną zakładkę. Z ostatniej przechodzi na pierwszą i odwrotnie.
↑↓
W zakładkach pionowych przenosi fokus na poprzednią lub następną zakładkę.
HomeEnd
Przenosi fokus na pierwszą lub ostatnią zakładkę.
EnterSpacja
Aktywuje zakładkę, na której jest fokus (gdy activateOnFocus jest wyłączone).
  • Ręczna aktywacja jest domyślna: strzałki przenoszą fokus, a Enter lub Spacja przełącza panel. Ustaw activateOnFocus na TabsList, gdy panele renderują się tanio — wtedy fokus i wybór przesuwają się razem.
  • Panele przyjmują fokus, dzięki czemu osoby korzystające z klawiatury dotrą do treści bez elementów interaktywnych; po przejściu do panelu klawiszem Tab widać pierścień fokusu.
  • Nazwij listę atrybutem aria-label, jeśli żaden widoczny nagłówek nie opisuje zakładek.

Dokumentacja API

Tabs

Komponent główny. Przyjmuje wszystkie propsy Tabs.Root z Base UI.

PropTypDomyślnie
value

Kontrolowana wartość aktywnej zakładki.

anyBrak wartości domyślnej
defaultValue

Zakładka aktywna na początku, w trybie niekontrolowanym.

any0
onValueChange

Wywoływana, gdy zmienia się aktywna zakładka.

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

Układ listy i kierunek nawigacji strzałkami.

"horizontal" | "vertical""horizontal"

TabsList

PropTypDomyślnie
variant

Kontrolka segmentowa albo pasek z podkreśleniem.

"default" | "line""default"
activateOnFocus

Aktywuje zakładki w miarę przesuwania fokusu strzałkami.

booleanfalse
loopFocus

Przenosi fokus z ostatniej zakładki z powrotem na pierwszą.

booleantrue

TabsTrigger

PropTypDomyślnie
valuewymagany

Identyfikuje zakładkę i jej panel.

anyBrak wartości domyślnej
disabled

Blokuje aktywację. Zakładka nadal przyjmuje fokus przy nawigacji strzałkami i jest odczytywana jako wyłączona.

booleanfalse

TabsContent

PropTypDomyślnie
valuewymagany

Wartość zakładki, do której należy ten panel.

anyBrak wartości domyślnej
keepMounted

Zostawia ukryty panel w DOM, zachowując jego stan.

booleanfalse

tabsListVariants

Generator klas dla TabsList, eksportowany na potrzeby własnych list o spójnym wyglądzie.