---
title: Tabs
description: Layered panels of content, with one panel displayed at a time.
---

<PackageInfo name="tabs" />

**bun**

```bash
bun add @data-slot/tabs
```

**npm**

```bash
npm install @data-slot/tabs
```

**pnpm**

```bash
pnpm add @data-slot/tabs
```

## Anatomy

Triggers and panels are paired by matching `data-value`. The indicator is optional.

```html
<div data-slot="tabs" data-default-value="initial-tab">
  <div data-slot="tabs-list">
    <button data-slot="tabs-trigger" data-value="unique-id">Label</button>
    <!-- Optional animated indicator -->
    <div data-slot="tabs-indicator"></div>
  </div>
  <div data-slot="tabs-content" data-value="unique-id">Panel content</div>
</div>
```

## Examples

### Switch between panels

<Example name="tabs" />

### Animated indicator

The optional `tabs-indicator` receives `--active-tab-left` and `--active-tab-width`, so it can slide under the active trigger.

<Example name="tabs" variant="extra" />

## API reference

### Initialization

#### `create(scope?)`

Auto-discover and bind all tabs instances in a scope (defaults to `document`).

```typescript
import { create } from "@data-slot/tabs";

const controllers = create(); // Returns TabsController[]
```

#### `createTabs(root, options?)`

Create a controller for a specific element.

```typescript
import { createTabs } from "@data-slot/tabs";

const tabs = createTabs(element, {
  defaultValue: "one",
  orientation: "horizontal",
  onValueChange: (value) => console.log(value),
});
```

### Slots

#### Runtime Slots

- `tabs` - Root element that manages the selected tab and activation mode.
- `tabs-list` - Required tab-list container that receives tablist semantics and orientation.
- `tabs-trigger` - Tab button identified by `data-value`; receives selected state and keyboard navigation. At least one trigger is required.
- `tabs-content` - Panel matched to its trigger by `data-value`; receives tabpanel semantics and is shown when selected.
- `tabs-indicator` - Optional animated highlight whose position and size follow the selected trigger.

#### Markup

```html
<div data-slot="tabs" data-default-value="initial-tab">
  <div data-slot="tabs-list">
    <button data-slot="tabs-trigger" data-value="unique-id">Label</button>
    <!-- Optional animated indicator -->
    <div data-slot="tabs-indicator"></div>
  </div>
  <div data-slot="tabs-content" data-value="unique-id">Panel content</div>
</div>
```

### Data Attributes

Options can also be set via data attributes on the root element. JS options take precedence.

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `data-default-value` | string | first enabled tab | Initial selected tab |
| `data-orientation` | string | `"horizontal"` | Tab orientation: horizontal, vertical |
| `data-activation-mode` | string | `"auto"` | Activation mode: auto, manual |

```html
<!-- Vertical tabs with manual activation -->
<div data-slot="tabs" data-orientation="vertical" data-activation-mode="manual">
  ...
</div>
```

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `defaultValue` | `string` | First enabled trigger's value | Initial selected tab |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Tab orientation for keyboard nav |
| `activationMode` | `"auto" \| "manual"` | `"auto"` | How tabs are activated with keyboard |
| `onValueChange` | `(value: string) => void` | `undefined` | Callback when selected tab changes |

### Controller

| Method/Property | Description |
|-----------------|-------------|
| `select(value)` | Select a tab by value |
| `value` | Currently selected value (readonly `string`) |
| `updateIndicator()` | Recalculate indicator position after layout changes |
| `destroy()` | Cleanup all event listeners |

### Events

#### Outbound Events

Listen for changes via custom events:

```javascript
element.addEventListener("tabs:change", (e) => {
  console.log("Selected tab:", e.detail.value);
});
```

#### Inbound Events

Control the tabs via events:

| Event | Detail | Description |
|-------|--------|-------------|
| `tabs:set` | `{ value: string }` | Select a tab programmatically |

```javascript
// Select a tab
element.dispatchEvent(
  new CustomEvent("tabs:set", { detail: { value: "two" } })
);
```

#### Deprecated Events

The following event is deprecated and will be removed in v1.0:

```javascript
// Deprecated: tabs:select event
element.dispatchEvent(
  new CustomEvent("tabs:select", { detail: { value: "two" } })
);

// Deprecated: string detail
element.dispatchEvent(
  new CustomEvent("tabs:select", { detail: "two" })
);
```

Use `tabs:set` with `{ value: string }` instead.

### Styling

#### Basic Styling

```css
/* Hidden panels */
[data-slot="tabs-content"][hidden] {
  display: none;
}

/* Active trigger */
[data-slot="tabs-trigger"][aria-selected="true"] {
  font-weight: bold;
  border-bottom: 2px solid currentColor;
}

/* Or use data-state */
[data-slot="tabs-trigger"][data-state="active"] {
  color: blue;
}

[data-slot="tabs-trigger"][data-state="inactive"] {
  color: gray;
}
```

#### Panel Activation Direction

Panels receive `data-activation-direction` after tab changes (not on initial mount):

- Horizontal: `left`, `right`
- Vertical: `up`, `down`

Use it for directional content animations:

```css
[data-slot="tabs-content"][data-activation-direction="right"] {
  animation: slide-in-from-right 200ms ease;
}

[data-slot="tabs-content"][data-activation-direction="left"] {
  animation: slide-in-from-left 200ms ease;
}
```

#### Animated Indicator

The indicator receives CSS variables for positioning:

```css
[data-slot="tabs-indicator"] {
  position: absolute;
  left: var(--active-tab-left);
  width: var(--active-tab-width);
  height: 2px;
  background: currentColor;
  transition: left 0.2s, width 0.2s;
}

/* Vertical orientation */
[data-slot="tabs-list"][aria-orientation="vertical"] [data-slot="tabs-indicator"] {
  top: var(--active-tab-top);
  height: var(--active-tab-height);
  width: 2px;
}
```

#### CSS Variables

| Variable | Description |
|----------|-------------|
| `--active-tab-left` | Left offset of active trigger |
| `--active-tab-width` | Width of active trigger |
| `--active-tab-top` | Top offset of active trigger |
| `--active-tab-height` | Height of active trigger |

#### Tailwind Example

```html
<div data-slot="tabs">
  <div data-slot="tabs-list" class="relative flex border-b">
    <button 
      data-slot="tabs-trigger" 
      data-value="one"
      class="px-4 py-2 aria-selected:text-blue-600"
    >
      Tab One
    </button>
    <div 
      data-slot="tabs-indicator" 
      class="absolute bottom-0 h-0.5 bg-blue-600 transition-all"
      style="left: var(--active-tab-left); width: var(--active-tab-width)"
    ></div>
  </div>
  <div data-slot="tabs-content" data-value="one" class="p-4">
    Content
  </div>
</div>
```

### Keyboard Navigation

The tables below describe `activationMode: "auto"` (the default). In `"manual"` mode, arrow keys, `Home`, and `End` only move focus; `Enter` or `Space` selects the focused tab. Disabled triggers are skipped.

#### Horizontal Orientation

| Key | Action |
|-----|--------|
| `ArrowLeft` | Select previous tab |
| `ArrowRight` | Select next tab |
| `Home` | Select first tab |
| `End` | Select last tab |

#### Vertical Orientation

| Key | Action |
|-----|--------|
| `ArrowUp` | Select previous tab |
| `ArrowDown` | Select next tab |
| `Home` | Select first tab |
| `End` | Select last tab |

### Accessibility

The component automatically handles:

- `role="tablist"` on list
- `role="tab"` on triggers
- `role="tabpanel"` on content
- `aria-orientation` on list
- `aria-selected` on triggers
- `aria-controls` linking triggers to panels
- `aria-labelledby` linking panels to triggers
- `tabindex` management (only the selected enabled tab is in the tab order)
