Tabs
Switches between panels of related content in place. Two looks: underline, a bar under the selected tab on a baseline across the row, and segmented, a raised segment in a sunken track. Each tab shows text, an icon and text, or only an icon.
Xem trước
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>Cách dùng
Khi nào dùng
- Split one page or card into a few sections the user switches between in place: Overview, Activity, Settings.
- Switch between views of the same content in a small space, such as files as a grid or a list, with the segmented look.
- A segmented control across a phone screen, every tab an equal share, with fullWidth.
Khi nào không dùng
- Going to another page. Use a link; tabs swap panels in place.
- Running an action, such as saving or exporting. Use Button.
- Picking a value for a form field. Use Select.
- The steps of a flow that must be done in order.
Biến thể
variant
Set on TabsList. underline: sections of a page or a card. segmented: switching views of the same content in a small space, such as Grid and List, or a filter next to other controls.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList variant="segmented">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>size
Set on TabsList. List height from size-element-*: sm 28px, md 32px (the default), lg 36px, xl 44px with a mouse; on touch md is 36px, lg 44px and xl 48px. Match the Button or Input next to it; use lg or xl for the main sections of a screen on touch.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList size="sm">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList size="lg">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList size="xl">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>Ví dụ
Default
Underline tabs for the sections of a page.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>With icons
An icon before every label, never on only some tabs.
import { IconChartLine, IconHome, IconSettings } from "@tabler/icons-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">
<IconHome size={16} />
Overview
</TabsTrigger>
<TabsTrigger value="activity">
<IconChartLine size={16} />
Activity
</TabsTrigger>
<TabsTrigger value="settings">
<IconSettings size={16} />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="settings" className="text-sm text-text-secondary">Name, members and access.</TabsContent>
</Tabs>Icon only
A segmented view switcher. Each tab names its view in aria-label.
import { IconLayoutGrid, IconList } from "@tabler/icons-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="grid">
<TabsList variant="segmented">
<TabsTrigger value="grid" iconOnly aria-label="Grid">
<IconLayoutGrid size={16} />
</TabsTrigger>
<TabsTrigger value="list" iconOnly aria-label="List">
<IconList size={16} />
</TabsTrigger>
</TabsList>
<TabsContent value="grid" className="text-sm text-text-secondary">Files as a grid of thumbnails.</TabsContent>
<TabsContent value="list" className="text-sm text-text-secondary">Files as a list with size and date.</TabsContent>
</Tabs>Full width
Segmented tabs that share the width of a narrow screen equally.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="week" className="w-full max-w-sm">
<TabsList variant="segmented" fullWidth>
<TabsTrigger value="day">Day</TabsTrigger>
<TabsTrigger value="week">Week</TabsTrigger>
<TabsTrigger value="month">Month</TabsTrigger>
</TabsList>
<TabsContent value="day" className="text-sm text-text-secondary">Changes from today.</TabsContent>
<TabsContent value="week" className="text-sm text-text-secondary">Changes from the last 7 days.</TabsContent>
<TabsContent value="month" className="text-sm text-text-secondary">Changes from the last 30 days.</TabsContent>
</Tabs>Disabled tab
Stays in the row but can't be selected; arrow keys skip it.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-full max-w-md">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="billing" disabled>Billing</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-text-secondary">Tokens, components and docs for this project.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">Changes from your team in the last 30 days.</TabsContent>
<TabsContent value="billing" className="text-sm text-text-secondary">Plan and invoices.</TabsContent>
</Tabs>Next tab loading
The user picked Activity, which is still loading: Overview stays on screen, dimmed and marked busy. In an app, pending is the isPending of the transition that changes the tab.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
<Tabs defaultValue="overview" className="w-80">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
<TabsContent value="overview" pending pendingLabel="Loading activity" className="text-sm text-text-secondary">Revenue is up 12% this month.</TabsContent>
<TabsContent value="activity" className="text-sm text-text-secondary">3 new comments.</TabsContent>
</Tabs>Nên và không nên
Nên
- Use underline for the sections of a page or a card: Overview, Activity, Settings.
- Use segmented to switch between views of the same content in a small space, such as Grid and List.
- Keep labels to one or two words in sentence case, and few enough tabs that the row fits without scrolling.
- In one list, give every tab an icon or none of them.
- Use iconOnly only for views whose icon is clear on its own, such as Grid and List, and give each one an aria-label.
- Match the list size to the Button or Input next to it.
- Use fullWidth for two to four short tabs across a phone screen or a card, so every tab is an equal, easy target.
- When a panel loads its content, switch tabs in a transition (useTransition) and pass isPending to TabsContent as pending: the current panel stays until the next one is ready. Show a Skeleton only for a panel that has never loaded.
Không nên
- Don't use tabs to go to another page. Tabs swap panels in place; use links to navigate.
- Don't use tabs for the steps of a flow that must be done in order.
- Don't use tabs to run an action. A tab selects a panel; use Button for actions.
- Don't put tabs inside a tab panel.
- Don't use fullWidth with many tabs or long labels: equal shares cut long labels short.
- Don't restyle colors through className. Pick a variant. If none fits, open an issue instead of overriding it.
- Don't blank the panel or swap in a spinner while the next tab loads.
Props
| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
|---|---|---|---|---|
| variant | enum | underline | segmented | underline | Set on TabsList. underline: sections of a page or a card. segmented: switching views of the same content in a small space, such as Grid and List, or a filter next to other controls. |
| size | enum | sm | md | lg | xl | md | Set on TabsList. List height from size-element-*: sm 28px, md 32px (the default), lg 36px, xl 44px with a mouse; on touch md is 36px, lg 44px and xl 48px. Match the Button or Input next to it; use lg or xl for the main sections of a screen on touch. |
| iconOnly | boolean | - | false | Set on TabsTrigger. A square tab that shows only its icon. Needs aria-label. |
| fullWidth | boolean | - | false | Set on TabsList. The list fills its container and every tab takes an equal share. For a segmented control across a narrow screen, or a card whose tabs span its width. |
| disabled | boolean | - | false | Set on TabsTrigger. The tab can't be selected, and arrow keys skip it. |
| defaultValue | string | - | - | Set on Tabs. The value of the tab selected on first render, when you don't control it. |
| value | string | - | - | Set on Tabs. The value of the selected tab, when you control it. Pair it with onValueChange. |
| onValueChange | function | - | - | Set on Tabs. Called with the value of the newly selected tab. |
| pending | boolean | - | false | TabsContent: the tab the user picked is still loading. Switch tabs in a transition (useTransition) so the current panel stays on screen, and pass its isPending: the panel dims to opacity-pending and is marked aria-busy instead of going blank. |
| pendingLabel | string | - | Loading | TabsContent: read by screen readers in a polite status while pending. |
Slot
| Tên | Bắt buộc | Mô tả |
|---|---|---|
| list | có | TabsList with one TabsTrigger per tab. Each TabsTrigger has a value. |
| label | không | The text of a tab: one or two words, sentence case. Overview, Activity, Settings. With iconOnly it moves to aria-label. |
| icon | không | Optional icon before the label, or alone with iconOnly. |
| content | có | One TabsContent per tab, with the same value as its TabsTrigger. |
Cấu tạo
| Phần | Mô tả |
|---|---|
| root | Tabs. Holds the selected value and stacks the list above the panels. |
| list | TabsList: the row of tabs. Sets variant and size for every tab in it, and takes the size-element-* height so it lines up with a Button or Input of the same size. |
| trigger | TabsTrigger: one tab. Stretches to the height of the list. |
| icon | Optional Tabler icon before the label; fill is allowed on the selected tab only. 16px in sm, md and lg, 20px in xl (DESIGN.md 9.1). |
| underline | The selection bar of the underline variant: an ::after on the selected tab, laid over the list's baseline. |
| content | TabsContent: the panel of one tab. Only the selected one is rendered. With pending it stays on screen, dims to opacity-pending (duration-medium) and is marked aria-busy, with a polite status next to it. |
| hitArea | Transparent ::before centered on each tab, never smaller than size-target-min (24px on mouse, 44px on touch), so small tabs stay a 44px touch target. |
Khả năng truy cập
Role: tablist
| Phím | Hành động |
|---|---|
| Tab | Moves focus into the tab list, onto the selected tab. Tab again moves focus to the selected panel. |
| ArrowRight, ArrowLeft | Moves focus to the next or previous tab and selects it. Wraps from the last tab to the first and back. |
| Home, End | Moves focus to the first or last tab and selects it. |
- TabsList is the tablist, each TabsTrigger a button with role tab, aria-selected and aria-controls, each TabsContent a tabpanel labelled by its tab.
- The selected tab follows focus, so arrow keys show each panel right away. For panels that load slowly, pass activationMode="manual" on Tabs: arrow keys only move focus, and Enter or Space selects.
- An icon-only tab has no visible text, so it needs an aria-label. TypeScript requires one when iconOnly is set.
- A disabled tab uses the native disabled attribute: it can't be selected, and arrow keys skip it.
- Selection is never shown by color alone: underline adds the bar, segmented the raised segment, on top of the stronger text color.
- The panel is focusable, so keyboard users can reach its text even when it holds no control. It shows the focus ring.
Cài đặt
npx shadcn add https://werewolf-ui.vercel.app/r/tabs.jsonLệnh này thêm luôn styles/werewolf.css, bộ token mà mọi component dùng, và các component khác mà nó cần. Import file đó một lần trong CSS gốc, ngay sau @import "tailwindcss", với đường dẫn tính từ file CSS đó (từ app/globals.css là @import "../styles/werewolf.css").