# Tabs

> React tabs with a sliding indicator that switch views of one page: underline for sections, pill for filters, segmented for a compact switch.

- Type: React component (Navigation)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add tabs`
- Docs: https://wingo-ui.com/components/tabs

## Interaction

Click or tap a tab and the indicator glides over to it on a smooth spring while the new panel slides in from the side you moved toward and the old one fades away. Tabs dip slightly under the finger. With the keyboard, Tab lands on the active tab, Left and Right (Up and Down when vertical) move between tabs, Home and End jump to the ends, and Tab again moves into the panel; in manual mode the arrows only move focus and Enter or Space opens the tab. When the tabs do not fit, the row scrolls sideways with soft fades at the edges and keeps the active tab in view. On a phone, triggers are 44px tall, and vertical tabs that get narrower than 32rem (a phone, a side panel) turn into a scrolling row above the content. Count badges roll when their number changes. With dir set to rtl the arrow keys, the edge fades and the panel slide mirror.

## Installation

```bash
npx wingo-ui@latest add tabs
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/tabs.json`

Files:

- `components/ui/tabs.tsx`

npm dependencies: `npm i @radix-ui/react-tabs motion`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))

## Usage

```tsx
import { Tabs, type TabsOption } from "@/components/ui/tabs"

const options: TabsOption[] = [
  { value: "active", label: "Active", badge: 12, content: <p>12 listings live in New York.</p> },
  { value: "pending", label: "Pending", badge: 3, content: <p>3 listings waiting for review.</p> },
  { value: "sold", label: "Sold", content: <p>Last sale: Sep 2, 2026.</p> },
  { value: "expired", label: "Expired", content: <p>No expired listings.</p> },
]

export function MyListings() {
  return <Tabs options={options} aria-label="My listings" />
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options?` | `TabsOption[]` |  | The tabs: { value, label, icon?, badge?, disabled?, content? }. A number badge renders a small count that hides at 0; content is the panel. Leave content out to use the tabs as a filter or a switch. |
| `value?` | `string` |  | The active tab, when you control it. A value that is not in options leaves no tab active. |
| `defaultValue?` | `string` | `""` | The tab active on the first render when uncontrolled. Empty, or a value that is not in options (a stale link, options that load later), starts on the first enabled option. |
| `onValueChange?` | `(value: string) => void` |  | Called with the new value. Also called with the first enabled tab when the active one is removed from options. |
| `aria-label?` | `string` | `""` | Names the tab list for screen readers when no visible heading does, e.g. "My listings". To name it after a heading on the page, pass aria-labelledby with the heading's id instead; both land on the list. |
| `variant?` | `"underline" \| "pill" \| "segmented"` | `"underline"` | underline: a 2px bar under the active label over a hairline baseline, for page sections. pill: the active tab gets a tinted pill, for filters. segmented: a raised thumb in a tinted track, for a compact two to five way switch. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Trigger height 32 / 40 / 48px with matching padding, type and icon size. On touch screens md grows to 44px and sm keeps its look with a 44px hit area. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The indicator color: the underline bar, the pill tint, the segmented thumb, and the active badge. neutral is black in light mode and white in dark mode. |
| `color?` | `string` | `""` | Any CSS color for the indicator. Replaces the tone; text on a segmented thumb turns black or white by lightness. Leave empty to use the tone. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of pills, the segmented track and the focus ring. Scales with the size (md is 8 / 10 / 12px); the segmented thumb is 2px tighter than its track. full makes capsules. |
| `orientation?` | `"horizontal" \| "vertical"` | `"horizontal"` | vertical puts the tabs in a column beside the content (at least 176px wide, labels start-aligned) for settings pages; the underline becomes a bar on the start edge and arrow keys go up and down. |
| `responsive?` | `boolean` | `true` | Vertical tabs only: when the tabs are narrower than 32rem (a CSS container query on their own width, so a phone, a side panel or a narrow card alike) the column turns into a scrolling row above the content and the arrow keys follow. Turn it off to keep the column everywhere. |
| `fullWidth?` | `boolean` | `false` | Triggers share the width equally. Segmented plus fullWidth is the usual header switch on a phone. |
| `scrollable?` | `boolean` | `true` | A row that does not fit scrolls sideways with edge fades and keeps the active tab in view (the list scrolls, never the page). Off, the triggers wrap onto a new line instead. |
| `animateContent?` | `boolean` | `true` | Panels slide 16px in the direction you moved and fade; the leaving panel steps out of the flow so nothing jumps. Off, panels swap instantly. Under reduced motion they only fade. |
| `activationMode?` | `"automatic" \| "manual"` | `"automatic"` | automatic opens a tab as soon as the arrow keys focus it. manual only moves focus and opens on Enter or Space, for panels that are slow to render. |
| `keepMounted?` | `boolean` | `false` | Keeps inactive panels in the DOM (hidden), so a half-filled form keeps what was typed when you switch away and back. |
| `classNames?` | `Partial<Record<"list" \| "trigger" \| "indicator" \| "icon" \| "label" \| "badge" \| "content", string>>` |  | Extra classes for the inner parts: the scrolling list, every trigger, the indicator, icons, labels, badges and panels. |
| `className?` | `string` |  | Extra classes for the root, merged last. Override the tone vars here, e.g. [--tone:var(--info)]. |

## Parts

### TabsRoot (component)

The compound root: value, defaultValue, onValueChange, variant, size, tone, color, radius, orientation, responsive, fullWidth, scrollable, animateContent, activationMode, keepMounted, classNames. Wrap a TabsList and your TabsContent panels in it, and pass defaultValue (or value): unlike Tabs, the root does not pick the first tab for you. Props: TabsRootProps.

### TabsList (component)

The scrolling tab list (role tablist). Renders the sliding indicator for you; loop (default true) lets the arrow keys wrap. Give it an aria-label when no heading names it. Props: { loop?: boolean } & ComponentProps<"div">.

### TabsTrigger (component)

One tab. A number badge renders a count badge in the active tone; any other node renders as is. Props: { value: string; icon?: ReactNode; badge?: ReactNode | number; disabled?: boolean; children: ReactNode }.

### TabsContent (component)

The panel for one value, focusable with a visible ring. Leaving panels are positioned against the nearest positioned parent (TabsRoot is one). Props: { value: string; children: ReactNode }.

## Examples

- **Variants**: underline for page sections, pill for filters, segmented for a compact switch.
- **Scrolling filters on a phone**: Pills with icons in a 360px card: the row scrolls with edge fades and keeps the chosen category in view.
- **Phone header switch**: segmented plus fullWidth shares the width equally; the panel slides the way you moved.
- **Vertical settings**: A column beside the forms when there is room, a scrolling row when the tabs are narrower than 32rem (like in this panel). keepMounted keeps what you typed.
- **Live badges**: A number badge rolls to the new count and hides at 0.
- **Manual activation, from the parts**: TabsRoot, TabsList, TabsTrigger and TabsContent. The arrow keys only move focus; Enter or Space opens the tab, for panels that are slow to render.
- **Custom color**: Any CSS color or a tone paints the indicator: a violet underline, a brand thumb, a teal pill.
- **Sizes and radius**: 32, 40 and 48px; the segmented thumb stays 2px inside its track at every radius.

## See also

- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))
- [Toggle Group](https://wingo-ui.com/components/toggle-group) ([Markdown](https://wingo-ui.com/components/toggle-group.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Toggle](https://wingo-ui.com/components/toggle) ([Markdown](https://wingo-ui.com/components/toggle.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))

---

Canonical: https://wingo-ui.com/components/tabs
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
