# 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.

## Usage

### When to use

- 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.

### When not to use

- 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.

## Variants

### 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.

Values: `underline`, `segmented`. Default: `underline`.

### 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.

Values: `sm`, `md`, `lg`, `xl`. Default: `md`.

## Examples

### Default

Underline tabs for the sections of a page.

```tsx
<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.

```tsx
<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.

```tsx
<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.

```tsx
<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.

```tsx
<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.

```tsx
<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>
```

## Do and don't

### Do

- 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.

### Don't

- 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

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `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. |

### Slots

| Name | Required | Description |
| --- | --- | --- |
| `list` | yes | TabsList with one TabsTrigger per tab. Each TabsTrigger has a value. |
| `label` | no | The text of a tab: one or two words, sentence case. Overview, Activity, Settings. With iconOnly it moves to aria-label. |
| `icon` | no | Optional icon before the label, or alone with iconOnly. |
| `content` | yes | One TabsContent per tab, with the same value as its TabsTrigger. |

## Anatomy

| Part | Description |
| --- | --- |
| `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. |

## Accessibility

Role: `tablist`

| Keys | Action |
| --- | --- |
| `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.

## Install

```bash
npx shadcn add https://werewolf-ui.vercel.app/r/tabs.json
```

This also adds `styles/werewolf.css`, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after `@import "tailwindcss"`, with a path relative to that CSS file (from `app/globals.css` it is `@import "../styles/werewolf.css"`).
