Tabs
Przełącza powiązane widoki o wspólnym kontekście — pokazuje jeden panel naraz, bez opuszczania strony.
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
- 1ListaPasek, na którym leżą zakładki: domyślnie kontrolka segmentowa, a z
variant="line"— cienka linia. - 2WyzwalaczZakładka. Krótka etykieta w formie rzeczownika, opcjonalnie z ikoną na początku.
- 3WskaźnikPojedynczy element, który przesuwa się do aktywnej zakładki.
- 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.jsonUż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.
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.
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.
"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.
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).
| Klawisz | Dział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
activateOnFocusnaTabsList, 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.
TabsList
TabsTrigger
TabsContent
tabsListVariants
Generator klas dla TabsList, eksportowany na potrzeby własnych list o spójnym wyglądzie.