---
title: Tabs
description: Switch between peer panels with managed focus and activation.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Tabs

> Switch between peer panels with managed focus and activation.

Use Tabs to switch between peer panels while staying in the same page context. Use navigation links when each destination needs its own URL and document state.

## Basic usage

```tsx
import { Tabs } from 'moraine'

export function Example() {
  return <Tabs items={[{ value: 'account', label: 'Account', content: 'Profile settings' }]} />
}
```

## Anatomy

```text
Tabs [component; slot=root]
├── list [slot]
│   ├── indicator [slot]
│   └── trigger [slot]
│       ├── leading [slot]
│       └── label [slot]
└── content [slot]
```

String labels use the label slot; JSX labels render directly inside the trigger.

## Usage

### Selected value

Use `value` with `onChange` for controlled tabs, or `defaultValue` for an initial local selection. Disabled items are skipped during selection and keyboard navigation.

For JSX panel content, use an item `content` getter so inactive panels are not constructed before they are rendered. This is especially important in SSR and MDX, where eagerly creating an omitted component can consume hydration keys that have no matching HTML.

```tsx
import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'
import { createSignal } from 'solid-js'

const TAB_ITEMS: TabsT.Item[] = [
  {
    value: 'account',
    label: 'Account',
    icon: 'i-lucide:user',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        Manage your account preferences, profile details, and email notifications.
      </div>
    ),
  },
  {
    value: 'security',
    label: 'Security',
    icon: 'i-lucide:shield',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        Configure two-factor authentication, active sessions, and API tokens.
      </div>
    ),
  },
  {
    value: 'billing',
    label: 'Billing',
    icon: 'i-lucide:credit-card',
    content: (
      <div class="text-xs text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg bg-card/40">
        View past invoices, payment methods, and current subscription usage.
      </div>
    ),
  },
]

export function SelectionUsage() {
  const [tab, setTab] = createSignal('account')

  return (
    <div class="max-w-md w-full space-y-3">
      <Tabs items={TAB_ITEMS} value={tab()} onChange={setTab} />
      <p class="text-xs text-muted-foreground">
        Active tab: <span class="text-foreground font-medium font-mono">{tab()}</span>
      </p>
    </div>
  )
}
```

### Activation and keyboard navigation

`activationMode="automatic"` selects the tab while arrow navigation moves focus. In manual mode, arrows move focus and Enter or Space selects. `loop` controls whether navigation wraps at the first and last enabled items.

```tsx
import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'

const TABS: TabsT.Item[] = [
  {
    value: 'tab1',
    label: 'Overview',
    content: (
      <div class="text-xs text-muted-foreground p-3">System status and resource metrics.</div>
    ),
  },
  {
    value: 'tab2',
    label: 'Analytics',
    content: (
      <div class="text-xs text-muted-foreground p-3">Traffic breakdowns and request volume.</div>
    ),
  },
  {
    value: 'tab3',
    label: 'Logs',
    content: (
      <div class="text-xs text-muted-foreground p-3">Real-time application execution stream.</div>
    ),
  },
]

export function ActivationUsage() {
  return (
    <div class="max-w-md w-full space-y-4">
      <div class="space-y-1.5">
        <span class="text-xs text-muted-foreground font-medium">
          Automatic activation (focus selects tab)
        </span>
        <Tabs items={TABS} activationMode="automatic" defaultValue="tab1" />
      </div>
      <div class="space-y-1.5">
        <span class="text-xs text-muted-foreground font-medium">
          Manual activation (Enter / Space selects tab)
        </span>
        <Tabs items={TABS} activationMode="manual" defaultValue="tab1" />
      </div>
    </div>
  )
}
```

### Keyboard interaction

Arrow keys follow the configured orientation. `activationMode="automatic"` selects while moving focus; manual activation waits for Enter or Space.

| Key                                 | Description                                          |
| ----------------------------------- | ---------------------------------------------------- |
| <kbd>Arrow keys</kbd>               | Moves focus among enabled tabs by orientation.       |
| <kbd>Home</kbd> / <kbd>End</kbd>    | Moves focus to the first or last enabled tab.        |
| <kbd>Enter</kbd> / <kbd>Space</kbd> | Activates the focused tab in manual mode.            |
| <kbd>Tab</kbd>                      | Leaves the tab list according to normal focus order. |

## Examples

### Activation mode

```tsx
import { Tabs } from 'moraine'
import type { TabsT } from 'moraine'

const SETTINGS_TABS: TabsT.Item[] = [
  {
    label: 'General',
    value: 'general',
    icon: 'i-lucide:sliders',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">General Preferences</h4>
        <p class="text-xs text-muted-foreground">
          Automatic activation triggers immediately on arrow navigation.
        </p>
      </div>
    ),
  },
  {
    label: 'Deployments',
    value: 'deployments',
    icon: 'i-lucide:rocket',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">Deployment Settings</h4>
        <p class="text-xs text-muted-foreground">
          Manual activation requires pressing Enter or Space to commit selection.
        </p>
      </div>
    ),
  },
  {
    label: 'Notifications',
    value: 'notifications',
    icon: 'i-lucide:bell',
    content: (
      <div class="p-3 b-1 b-border rounded-xl bg-card/40 space-y-2">
        <h4 class="text-sm font-semibold">Alert Rules</h4>
        <p class="text-xs text-muted-foreground">Configure webhook dispatch channels.</p>
      </div>
    ),
  },
]

export function ActivationMode() {
  return (
    <div class="gap-6 grid max-w-2xl sm:grid-cols-2">
      <div class="space-y-2">
        <span class="text-xs text-muted-foreground tracking-wider font-semibold uppercase">
          Automatic Activation
        </span>
        <Tabs items={SETTINGS_TABS} activationMode="automatic" defaultValue="general" />
      </div>
      <div class="space-y-2">
        <span class="text-xs text-muted-foreground tracking-wider font-semibold uppercase">
          Manual Activation (Enter / Space)
        </span>
        <Tabs items={SETTINGS_TABS} activationMode="manual" defaultValue="general" />
      </div>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `tabs`, `tabs-trigger` | Present when the component, slot, or item is disabled. |
| `data-highlighted` | `tabs-trigger` | Present when the item is highlighted by pointer or keyboard navigation. |
| `data-selected` | `tabs-trigger`, `tabs-content` | Present when the item or tab is selected. |

## Props

Props for the Tabs component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| activationMode | 'automatic' \| 'manual' \| undefined | 'automatic' | Whether keyboard navigation activates the tab immediately or waits for confirmation. |
| defaultValue | string \| undefined | — | Default active tab value for uncontrolled usage. |
| disabled | boolean \| undefined | false | Whether the tab list is disabled. |
| id | string \| undefined | — | Unique identifier for the tabs root element. |
| items | ({<br>  /** Label to display on the tab trigger. */<br>  label?: JSX.Element;<br>  /** Icon to display next to the label. */<br>  icon?: IconT.Name;<br>  /** Unique value for the tab. */<br>  value?: string;<br>  /** Content to display when the tab is active. */<br>  content?: JSX.Element;<br>  /** Whether the tab is disabled. */<br>  disabled?: boolean;<br>})[] \| undefined | — | Array of tabs to display. |
| loop | boolean \| undefined | true | Whether arrow-key navigation wraps from the ends. |
| onChange | ((value: string) => void) \| undefined | — | Callback when the active tab changes. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'horizontal' | The orientation of the tab list. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| value | string \| undefined | — | Controlled active tab value. |
| variant | 'pill' \| 'link' \| undefined | 'pill' | Visual treatment of the component. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
