# Tabs (/ui/components/react/tabs)



<Playground component="tabs" />

## Usage [#usage]

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'
```

```tsx
<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="activity">Activity</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">A snapshot of your workspace.</TabsContent>
  <TabsContent value="activity">Every change, newest first.</TabsContent>
  <TabsContent value="settings">Workspace name and integrations.</TabsContent>
</Tabs>
```

`Tabs` lets a single region host several panels and shows one at a time. Each `TabsTrigger` is matched to a `TabsContent` by its `value`; selecting a trigger reveals its panel and slides the active indicator across the list. Drive it uncontrolled with `defaultValue`, or controlled with `value` + `onValueChange`.

The visual style is set once on the root and flows down through context: `variant` (`pill` or `line`), `size` (`sm`/`md`/`lg`), and `orientation` (`horizontal` or `vertical`). Override any single part by passing the same prop directly to a `TabsList` or `TabsTrigger`.

Reach for `Tabs` to switch between **alternate views of the same area**. For navigating between pages or routes, use [`Navigation`](/ui/components/react/navigation) or a [`Breadcrumb`](/ui/components/react/breadcrumb) instead — tabs are for in-place content, not page navigation.

## Examples [#examples]

### Default [#default]

The `pill` variant: a filled track with a sliding white pill behind the active tab. Set the starting tab with `defaultValue` and pair every `TabsTrigger` with a `TabsContent` of the same `value`.

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'

export default function TabsDefault() {
  return (
    <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-foreground-muted text-sm">
        A snapshot of your workspace — recent files, members, and usage.
      </TabsContent>
      <TabsContent value="activity" className="text-foreground-muted text-sm">
        Every change, comment, and deploy across the team, newest first.
      </TabsContent>
      <TabsContent value="settings" className="text-foreground-muted text-sm">
        Manage the workspace name, visibility, and connected integrations.
      </TabsContent>
    </Tabs>
  )
}
```

### Variants [#variants]

`pill` rides on a filled track; `line` drops the track for a minimal underline that slides under the active tab. Pick `line` for dense layouts or when the tabs sit directly above their content.

```tsx
import { Tabs, TabsList, TabsTrigger } from '@appica/ui-react/tabs'

export default function TabsVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-8">
      <Tabs defaultValue="overview" variant="pill">
        <TabsList>
          <TabsTrigger value="overview">Overview</TabsTrigger>
          <TabsTrigger value="activity">Activity</TabsTrigger>
          <TabsTrigger value="settings">Settings</TabsTrigger>
        </TabsList>
      </Tabs>
      <Tabs defaultValue="overview" variant="line">
        <TabsList>
          <TabsTrigger value="overview">Overview</TabsTrigger>
          <TabsTrigger value="activity">Activity</TabsTrigger>
          <TabsTrigger value="settings">Settings</TabsTrigger>
        </TabsList>
      </Tabs>
    </div>
  )
}
```

### Sizes [#sizes]

Three sizes scale the triggers' padding and text together: `sm`, `md` (default), and `lg`. Set `size` on the root and it flows to every trigger.

```tsx
import { Tabs, TabsList, TabsTrigger } from '@appica/ui-react/tabs'

export default function TabsSizes() {
  return (
    <div className="flex flex-col items-center gap-6">
      {(['sm', 'md', 'lg'] as const).map((size) => (
        <Tabs key={size} defaultValue="overview" size={size}>
          <TabsList>
            <TabsTrigger value="overview">Overview</TabsTrigger>
            <TabsTrigger value="activity">Activity</TabsTrigger>
            <TabsTrigger value="settings">Settings</TabsTrigger>
          </TabsList>
        </Tabs>
      ))}
    </div>
  )
}
```

### Vertical [#vertical]

Set `orientation="vertical"` to stack the triggers in a column beside their panels — handy for settings screens. The arrow keys follow the axis (↑/↓), and the active indicator runs vertically.

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'

export default function TabsVertical() {
  return (
    <Tabs defaultValue="general" orientation="vertical" className="w-full max-w-md">
      <TabsList>
        <TabsTrigger value="general">General</TabsTrigger>
        <TabsTrigger value="members">Members</TabsTrigger>
        <TabsTrigger value="billing">Billing</TabsTrigger>
      </TabsList>
      <TabsContent value="general" className="text-foreground-muted text-sm">
        Workspace name, URL slug, and default language.
      </TabsContent>
      <TabsContent value="members" className="text-foreground-muted text-sm">
        Invite teammates and manage their roles and permissions.
      </TabsContent>
      <TabsContent value="billing" className="text-foreground-muted text-sm">
        Your current plan, invoices, and payment method.
      </TabsContent>
    </Tabs>
  )
}
```

### With icons & badges [#with-icons--badges]

Drop an [icon](/ui/icons) marked `data-icon="start"` before a trigger's label and it's sized and spaced automatically. The `data-icon` attribute also tells the trigger to tighten its padding on that side. Triggers accept any children, so a trailing [`Badge`](/ui/components/react/badge) — say a count — sits inline after the label.

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'
import { Badge } from '@appica/ui-react/badge'
import { LayoutGrid, Activity, Settings } from '@appica/icons-react'

export default function TabsIcons() {
  return (
    <Tabs defaultValue="overview" className="w-full max-w-md">
      <div className="scrollbar-none overflow-x-auto [&::-webkit-scrollbar]:hidden">
        <TabsList>
          <TabsTrigger value="overview">
            <LayoutGrid data-icon="start" />
            Overview
          </TabsTrigger>
          <TabsTrigger value="activity">
            <Activity data-icon="start" />
            Activity
            <Badge variant="outline" size="sm">
              12
            </Badge>
          </TabsTrigger>
          <TabsTrigger value="settings">
            <Settings data-icon="start" />
            Settings
          </TabsTrigger>
        </TabsList>
      </div>
      <TabsContent value="overview" className="text-foreground-muted text-sm">
        A snapshot of your workspace at a glance.
      </TabsContent>
      <TabsContent value="activity" className="text-foreground-muted text-sm">
        Every change across the team, newest first.
      </TabsContent>
      <TabsContent value="settings" className="text-foreground-muted text-sm">
        Workspace name, visibility, and integrations.
      </TabsContent>
    </Tabs>
  )
}
```

### Icon-only [#icon-only]

Give each trigger an `icon-*` size (`icon-sm`/`icon-md`/`icon-lg`) for square, label-less tabs — a compact view switcher, for instance. Pass an `aria-label` so each tab still has an accessible name.

```tsx
import { Tabs, TabsList, TabsTrigger } from '@appica/ui-react/tabs'
import { LayoutGrid, List, ChartBar } from '@appica/icons-react'

export default function TabsIconOnly() {
  return (
    <Tabs defaultValue="grid">
      <TabsList>
        <TabsTrigger value="grid" size="icon-md" aria-label="Grid view">
          <LayoutGrid />
        </TabsTrigger>
        <TabsTrigger value="list" size="icon-md" aria-label="List view">
          <List />
        </TabsTrigger>
        <TabsTrigger value="chart" size="icon-md" aria-label="Chart view">
          <ChartBar />
        </TabsTrigger>
      </TabsList>
    </Tabs>
  )
}
```

### Controlled [#controlled]

Hold the active tab in state with `value` + `onValueChange` to drive it from elsewhere — here Back/Next buttons step through a wizard while the tabs reflect the current step.

```tsx
'use client'

import * as React from 'react'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'
import { Button } from '@appica/ui-react/button'

const STEPS = ['account', 'profile', 'review'] as const
type Step = (typeof STEPS)[number]

export default function TabsControlled() {
  const [step, setStep] = React.useState<Step>('account')
  const index = STEPS.indexOf(step)
  const goTo = (target: number) => setStep(STEPS[Math.min(Math.max(target, 0), STEPS.length - 1)]!)

  return (
    <div className="flex w-full max-w-sm flex-col gap-5">
      <Tabs value={step} onValueChange={(value) => setStep(value as Step)}>
        <TabsList>
          <TabsTrigger value="account">Account</TabsTrigger>
          <TabsTrigger value="profile">Profile</TabsTrigger>
          <TabsTrigger value="review">Review</TabsTrigger>
        </TabsList>
        <TabsContent value="account" className="text-foreground-muted text-sm">
          Step 1 — choose a username and password.
        </TabsContent>
        <TabsContent value="profile" className="text-foreground-muted text-sm">
          Step 2 — tell us a little about yourself.
        </TabsContent>
        <TabsContent value="review" className="text-foreground-muted text-sm">
          Step 3 — review everything and submit.
        </TabsContent>
      </Tabs>

      <div className="flex gap-2">
        <Button variant="outline" size="sm" disabled={index === 0} onClick={() => goTo(index - 1)}>
          Back
        </Button>
        <Button size="sm" disabled={index === STEPS.length - 1} onClick={() => goTo(index + 1)}>
          Next
        </Button>
      </div>
    </div>
  )
}
```

### Disabled [#disabled]

Add `disabled` to a `TabsTrigger` to make it non-interactive; arrow-key navigation skips over it.

```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@appica/ui-react/tabs'

export default function TabsDisabled() {
  return (
    <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-foreground-muted text-sm">
        The billing tab is disabled until you upgrade to a paid plan.
      </TabsContent>
      <TabsContent value="activity" className="text-foreground-muted text-sm">
        Every change across the team, newest first.
      </TabsContent>
    </Tabs>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, indicator placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The tabs flow from the right, the active indicator slides along the resolved direction, and ←/→ arrow keys swap roles. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="tabs" />

## API reference [#api-reference]

`Tabs` wraps [Base UI's Tabs](https://base-ui.com/react/components/tabs). The root provides the `variant`/`size`/`orientation` context; each part forwards `ref` and its remaining native attributes to the underlying element.

### Tabs [#tabs]

The root. Owns the selected value and renders a `<div>`.

| Prop            | <ColMinWidth width="200">Type</ColMinWidth>                   | Default        | <ColMinWidth width="220">Description</ColMinWidth>                                  |
| --------------- | ------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------- |
| `defaultValue`  | <Code>string \| number \| null</Code>                         | —              | The initially selected tab's `value`, for uncontrolled use.                         |
| `value`         | <Code>string \| number \| null</Code>                         | —              | The selected tab's `value`, for controlled use. Pair with `onValueChange`.          |
| `onValueChange` | <Code>(value, eventDetails) => void</Code>                    | —              | Called when the selected tab changes.                                               |
| `variant`       | <Code>'pill' \| 'line'</Code>                                 | `'pill'`       | Visual style, shared with the list and triggers via context.                        |
| `size`          | <Code>'sm' \| 'md' \| 'lg'</Code>                             | `'md'`         | Trigger sizing, shared via context.                                                 |
| `orientation`   | <Code>'horizontal' \| 'vertical'</Code>                       | `'horizontal'` | Layout axis and the direction arrow keys move focus. Exposed as `data-orientation`. |
| `render`        | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —              | Replace the rendered element, or compose it with another component.                 |
| `className`     | <Code>string \| ((state) => string)</Code>                    | —              | Extra classes, merged via `tailwind-merge`. May be a function of state.             |

### TabsList [#tabslist]

The row (or column) of triggers, with the sliding active indicator built in. Renders a `<div role="tablist">`.

| Prop              | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>                                   |
| ----------------- | ------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| `variant`         | <Code>'pill' \| 'line'</Code>                                 | context | Override the root's variant for this list.                                           |
| `size`            | <Code>'sm' \| 'md' \| 'lg'</Code>                             | context | Override the root's size for this list.                                              |
| `activateOnFocus` | `boolean`                                                     | `false` | Activate a tab as soon as it's focused with the arrow keys, not only on click/Enter. |
| `loopFocus`       | `boolean`                                                     | `true`  | Whether arrow-key focus wraps from the last trigger to the first and back.           |
| `render`          | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the rendered element, or compose it with another component.                  |
| `className`       | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                                          |

### TabsTrigger [#tabstrigger]

One tab button. Renders a `<button role="tab">`.

| Prop           | <ColMinWidth width="200">Type</ColMinWidth>                              | Default | <ColMinWidth width="220">Description</ColMinWidth>                  |
| -------------- | ------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------- |
| `value`        | <Code>string \| number</Code>                                            | —       | **Required.** Identifies the tab and matches it to a `TabsContent`. |
| `variant`      | <Code>'pill' \| 'line'</Code>                                            | context | Override the variant for this trigger.                              |
| `size`         | <Code>'sm' \| 'md' \| 'lg' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'</Code> | context | Override the size; `icon-*` makes a square, label-less trigger.     |
| `disabled`     | `boolean`                                                                | `false` | Make the tab non-interactive and skip it during arrow navigation.   |
| `nativeButton` | `boolean`                                                                | `true`  | Whether the element passed to `render` is a native `<button>`.      |
| `render`       | <Code>ReactElement \| ((props, state) => ReactElement)</Code>            | —       | Replace the rendered element, or compose it with another component. |
| `className`    | <Code>string \| ((state) => string)</Code>                               | —       | Extra classes, merged via `tailwind-merge`.                         |

### TabsContent [#tabscontent]

The panel revealed by its matching trigger. Renders a `<div role="tabpanel">`.

| Prop          | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>                  |
| ------------- | ------------------------------------------------------------- | ------- | ------------------------------------------------------------------- |
| `value`       | <Code>string \| number</Code>                                 | —       | **Required.** Matches the trigger of the same `value`.              |
| `keepMounted` | `boolean`                                                     | `false` | Keep inactive panels mounted (hidden) instead of unmounting them.   |
| `render`      | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the rendered element, or compose it with another component. |
| `className`   | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                         |

## Accessibility [#accessibility]

* The parts map to the WAI-ARIA tabs pattern: `TabsList` is a `role="tablist"`, each `TabsTrigger` a `role="tab"`, and each `TabsContent` a `role="tabpanel"` wired together with `aria-controls`/`aria-labelledby`.
* The list is a single tab stop: **Tab** moves focus into the active trigger, and the **arrow keys** move focus between triggers — ←/→ when horizontal, ↑/↓ when vertical — wrapping unless `loopFocus` is `false`.
* By default a tab activates on **Enter**/**Space**; set `activateOnFocus` to switch panels as focus moves instead.
* `disabled` triggers are removed from the tab order and skipped by arrow navigation.
* An icon-only `TabsTrigger` has no visible text, so give it an `aria-label`.
* The indicator and panel transitions honour `prefers-reduced-motion`.
