# Tabs

Accessible React tabs for Next.js, with an underline or pill that slides to the active tab and panels that glide in, built on Radix Tabs.

Source: https://ui.fewtone.com/components/tabs

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/tabs.json
```

## Usage

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

export function Settings() {
  return (
    <Tabs defaultValue="account">
      <TabsList aria-label="Settings">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
      </TabsList>
      <TabsContent value="account">Change your name and email.</TabsContent>
      <TabsContent value="password">Change your password.</TabsContent>
    </Tabs>
  )
}
```

## API reference

### Tabs

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | The tab shown, when you control it. |
| `defaultValue` | `string` | – | The tab shown at first. |
| `onValueChange` | `(value: string) => void` | – | Called with the new tab when the user picks one. |
| `activationMode` | `"automatic" \| "manual"` | `"automatic"` | Automatic shows a tab's panel as soon as the arrow keys reach it; manual waits for Enter or Space. |
| `dir` | `"ltr" \| "rtl"` | – | The reading direction. The arrow keys follow it, and a panel still slides in from the side of its tab. |

### TabsList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"underline" \| "pill"` | `"underline"` | Underline: a line slides along a hairline under the tabs. Pill: a filled pill slides on a muted track, like Switcher. |
| `loop` | `boolean` | `true` | The arrow keys wrap from the last tab to the first. |

### TabsTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | The panel this tab shows. |
| `disabled` | `boolean` | `false` | Skips the tab. |

### TabsContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | The tab that shows this panel. |
| `forceMount` | `true` | – | Keeps the panel's content mounted while it's hidden, so it keeps its state (a half-filled form) and stays in the HTML. |

**When to use it:** Use Tabs to switch between panels of related content in one place, like settings sections or Preview and Code. Use Switcher when the options change one view rather than each showing its own panel, Accordion when people should scan several sections in order, and links to move between pages.
**Built on:** Radix Tabs.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
**npm packages (installed by the CLI):** class-variance-authority@^0.7.1, motion@^14.0.0, radix-ui@^1.7.0.

## Examples

### Underline and pill

Pick a tab and the underline or pill slides to it. The new panel comes in from the side you moved towards while its height glides to fit.

```tsx
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useId } from "react";

const variants = [
  { variant: "underline", label: "Underline" },
  { variant: "pill", label: "Pill" },
] as const;

/** One settings set: each panel is a different height, so switching shows it gliding to its size */
function Settings({ id, variant, label }: { id: string; variant: "underline" | "pill"; label: string }) {
  return (
    <Tabs defaultValue="account">
      <TabsList variant={variant} aria-label={`Settings, ${label.toLowerCase()} style`}>
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
        <TabsTrigger value="notifications">Notifications</TabsTrigger>
      </TabsList>
      <TabsContent value="account" className="grid gap-2">
        <label htmlFor={`${id}-name`} className="text-sm font-medium">
          Name
        </label>
        <Input id={`${id}-name`} defaultValue="Ada Lovelace" aria-describedby={`${id}-name-hint`} />
        <p id={`${id}-name-hint`} className="text-sm text-muted-foreground">
          Shown on your profile and in emails.
        </p>
      </TabsContent>
      <TabsContent value="password" className="grid gap-4">
        <div className="grid gap-2">
          <label htmlFor={`${id}-current`} className="text-sm font-medium">
            Current password
          </label>
          <Input id={`${id}-current`} type="password" autoComplete="current-password" />
        </div>
        <div className="grid gap-2">
          <label htmlFor={`${id}-new`} className="text-sm font-medium">
            New password
          </label>
          <Input id={`${id}-new`} type="password" autoComplete="new-password" />
        </div>
      </TabsContent>
      <TabsContent value="notifications" className="grid gap-4">
        <div className="flex items-center justify-between gap-4">
          <label htmlFor={`${id}-email`} className="text-sm font-medium">
            Email updates
          </label>
          <Switch id={`${id}-email`} defaultChecked />
        </div>
        <div className="flex items-center justify-between gap-4">
          <label htmlFor={`${id}-push`} className="text-sm font-medium">
            Push notifications
          </label>
          <Switch id={`${id}-push`} />
        </div>
      </TabsContent>
    </Tabs>
  );
}

/*
 * Both looks side by side, named underneath. Top-aligned, so switching one set never moves the other.
 */
export default function TabsVariants() {
  const id = useId();
  return (
    <ul className="flex w-full flex-wrap items-start justify-center gap-x-10 gap-y-8">
      {variants.map(({ variant, label }) => (
        <li key={variant} className="flex w-72 flex-col gap-4">
          <Settings id={`${id}-${variant}`} variant={variant} label={label} />
          <span className="text-center text-xs text-muted-foreground">{label}</span>
        </li>
      ))}
    </ul>
  );
}
```

### Icons

Icons sit before the label at the icon size. Give icon-only tabs an aria-label.

```tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ActivityIcon, LayoutDashboardIcon, SettingsIcon } from "lucide-react";

export default function TabsIcons() {
  return (
    <Tabs defaultValue="overview" className="w-full max-w-sm">
      <TabsList variant="pill" aria-label="Project">
        <TabsTrigger value="overview">
          <LayoutDashboardIcon aria-hidden />
          Overview
        </TabsTrigger>
        <TabsTrigger value="activity">
          <ActivityIcon aria-hidden />
          Activity
        </TabsTrigger>
        <TabsTrigger value="settings">
          <SettingsIcon aria-hidden />
          Settings
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview" className="text-sm text-muted-foreground">
        Three deployments this week, all healthy.
      </TabsContent>
      <TabsContent value="activity" className="text-sm text-muted-foreground">
        Ada merged two pull requests and opened an issue about the sign-in page.
      </TabsContent>
      <TabsContent value="settings" className="text-sm text-muted-foreground">
        Name, domain and who can see this project.
      </TabsContent>
    </Tabs>
  );
}
```

### Disabled

A disabled tab can't be picked, and the arrow keys skip it.

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

export default function TabsDisabled() {
  return (
    <Tabs defaultValue="account" className="w-full max-w-xs">
      <TabsList aria-label="Settings">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="billing" disabled>
          Billing
        </TabsTrigger>
        <TabsTrigger value="team">Team</TabsTrigger>
      </TabsList>
      <TabsContent value="account" className="text-sm text-muted-foreground">
        Your name and email, as others see them.
      </TabsContent>
      <TabsContent value="billing" className="text-sm text-muted-foreground">
        Your plan, payment method and invoices.
      </TabsContent>
      <TabsContent value="team" className="text-sm text-muted-foreground">
        Invite people and choose what they can change.
      </TabsContent>
    </Tabs>
  );
}
```

## Related

- [Switcher](https://ui.fewtone.com/components/switcher.md): An accessible React segmented control for Next.js: one choice from a few options, with a pill that slides to the one you pick.
- [Accordion](https://ui.fewtone.com/components/accordion.md): An accessible React accordion for Next.js: sections that open and close with a smooth height change, as one card, separate cards or plain rows. Built on Radix.
- [Toggle Group](https://ui.fewtone.com/components/toggle-group.md): An accessible React toggle group for Next.js: buttons you switch on and off independently, like Bold, Italic and Underline in a toolbar.
