# Tabs

> A family of composable components for tabbed interfaces with built-in keyboard navigation, ARIA support, and optional responsive accordion behavior.

# Tabs

## Overview

Use the Tabs family when you need a tabbed interface where one content panel is visible at a time, selected by clicking its corresponding trigger. The family is composed, not standalone — the root component manages shared state and keyboard navigation, while child components define the trigger list and content panels. Tabs work well for product feature sections, settings panels, pricing tiers, and any content that benefits from organizing information into mutually exclusive views.

## Authoring Structure

```
Tabs
├── TabsList
│   ├── TabsTrigger (1st)
│   ├── TabsTrigger (2nd)
│   └── TabsTrigger (3rd)
├── TabsContent (1st)
├── TabsContent (2nd)
└── TabsContent (3rd)
```

### Placement Rules

| Component | Placement | Role |
|---|---|---|
| **Tabs** | Top-level wrapper. | Owns state, keyboard navigation, and shared options. |
| **TabsList** | Direct child of `Tabs`. | Container for trigger buttons. Receives `role="tablist"`. |
| **TabsTrigger** | Inside `TabsList`. | Interactive button that activates its paired content panel. |
| **TabsContent** | Direct child of `Tabs`, sibling to `TabsList`. | The content panel that shows or hides based on the active trigger. |

### Pairing Rule

Triggers and content panels are paired by **position** — the first `TabsTrigger` inside `TabsList` pairs with the first `TabsContent` that is a direct child of `Tabs`. The second pairs with the second, and so on. This is purely positional; labels and IDs are not used for matching. If trigger and content counts mismatch, extra triggers are disabled and extra content panels stay hidden.

## Quick Start

<CodeExample title="Basic horizontal tabs" language="jsx">
{`<OmeTabs settings='{{"orientation":"horizontal"}}'>
  {#slot default}
    <OmeTabsList>
      {#slot default}
        <OmeTabsTrigger content='{{"label":"Overview"}}' />
        <OmeTabsTrigger content='{{"label":"Details"}}' />
        <OmeTabsTrigger content='{{"label":"Changelog"}}' />
      {/slot}
    </OmeTabsList>
    <OmeTabsContent>
      {#slot default}Overview content goes here.{/slot}
    </OmeTabsContent>
    <OmeTabsContent>
      {#slot default}Details content goes here.{/slot}
    </OmeTabsContent>
    <OmeTabsContent>
      {#slot default}Changelog content goes here.{/slot}
    </OmeTabsContent>
  {/slot}
</OmeTabs>`}
</CodeExample>

<CodeExample title="Vertical tabs with responsive accordion" language="jsx">
{`<OmeTabs
  settings='{{"orientation":"vertical"}}'
  responsiveAccordion='{{"enableResponsiveAccordion":true,"accordionBreakpoint":"768","showAccordionIndicator":true}}'
>
  {#slot default}
    <OmeTabsList styling='{{"class":"ome-tabs-list-default vertical-nav"}}'>
      {#slot default}
        <OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
          {#slot default}Features{/slot}
        </OmeTabsTrigger>
        <OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
          {#slot default}Pricing{/slot}
        </OmeTabsTrigger>
        <OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
          {#slot default}FAQ{/slot}
        </OmeTabsTrigger>
      {/slot}
    </OmeTabsList>
    <OmeTabsContent>
      {#slot default}Features content.{/slot}
    </OmeTabsContent>
    <OmeTabsContent>
      {#slot default}Pricing content.{/slot}
    </OmeTabsContent>
    <OmeTabsContent>
      {#slot default}FAQ content.{/slot}
    </OmeTabsContent>
  {/slot}
</OmeTabs>`}
</CodeExample>

---

## Family Components

### Tabs (Root)

The root component wraps all lists and panels, and manages shared behavior: active tab state, keyboard navigation, orientation, and optional responsive accordion mode. It does not render any visible UI itself — only a container element.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="div">
    HTML tag for the root element.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="orientation" type={`"horizontal" | "vertical"`} defaultValue="horizontal">
    Controls layout direction and keyboard behavior. `"horizontal"` uses ArrowLeft/ArrowRight for navigation. `"vertical"` uses ArrowUp/ArrowDown.
  </Prop>
  <Prop name="defaultIndex" type="string" defaultValue='"0"'>
    Zero-based index of the tab that should be active on initial load. If the index is invalid or points to a disabled trigger, it falls back to the first enabled trigger.
  </Prop>
  <Prop name="disabled" type="boolean" defaultValue="false">
    Disables the entire tabs interface when `true`. Individual triggers can still override this with their own `disabled` prop.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-tabs-root-default">
    CSS class applied to the root element.
  </Prop>
</PropsTable>

<PropsTable group="Responsive Accordion">
  <Prop name="enableResponsiveAccordion" type="boolean" defaultValue="false">
    Enables responsive accordion mode. When the viewport width is below `accordionBreakpoint`, the runtime restructures the tabs into an accordion.
  </Prop>
  <Prop name="accordionBreakpoint" type="string" defaultValue='"768"'>
    Viewport width in pixels below which accordion mode activates. Only applies when `enableResponsiveAccordion` is `true`.
  </Prop>
  <Prop name="etchPreviewMode" type={`"auto" | "tabs" | "accordion"`} defaultValue="auto">
    Controls Etch editor preview behavior. `"auto"` follows the canvas width. `"tabs"` always shows tabs. `"accordion"` always shows the accordion. Only applies when responsive accordion is enabled.
  </Prop>
  <Prop name="accordionAnimationDuration" type="string" defaultValue='"300"'>
    Animation duration in milliseconds for accordion panel expand/collapse. Only applies when responsive accordion is enabled.
  </Prop>
  <Prop name="showAccordionIndicator" type="boolean" defaultValue="false">
    Adds indicator spans to triggers in accordion mode. Only applies when responsive accordion is enabled.
  </Prop>
  <Prop name="accordionItemClass" type="class">
    CSS class applied to each generated accordion item wrapper. Only applies when responsive accordion is enabled.
  </Prop>
  <Prop name="accordionHeaderClass" type="class">
    CSS class applied to each generated accordion header wrapper. Only applies when responsive accordion is enabled.
  </Prop>
  <Prop name="accordionIndicatorClass" type="class">
    CSS class applied to generated accordion indicators. Only applies when `showAccordionIndicator` is `true`.
  </Prop>
</PropsTable>

#### Keyboard Behavior — Tabs Mode

When a trigger has focus in tabs mode:

| Key | Action |
|---|---|
| `Enter` / `Space` | Activate the focused trigger. |
| `ArrowRight` | Move focus to the next trigger and activate it (horizontal). Wraps to first. RTL-aware. |
| `ArrowLeft` | Move focus to the previous trigger and activate it (horizontal). Wraps to last. RTL-aware. |
| `ArrowDown` | Move focus to the next trigger and activate it (vertical). Wraps to first. |
| `ArrowUp` | Move focus to the previous trigger and activate it (vertical). Wraps to last. |
| `Home` | Focus and activate the first enabled trigger. |
| `End` | Focus and activate the last enabled trigger. |

In horizontal tabs, arrow keys automatically reverse in RTL layouts.

#### Keyboard Behavior — Accordion Mode

When the responsive accordion is active, keyboard behavior changes to match the Accordion component:

| Key | Action |
|---|---|
| `Enter` / `Space` | Toggle the associated panel. |
| `ArrowDown` | Move focus to the next trigger (does **not** activate). |
| `ArrowUp` | Move focus to the previous trigger (does **not** activate). |
| `Home` | Move focus to the first trigger (does **not** activate). |
| `End` | Move focus to the last trigger (does **not** activate). |

Focus movement in accordion mode does **not** auto-activate — you must click or press `Enter`/`Space` to open a panel.

#### Responsive Accordion

When enabled and the viewport drops below `accordionBreakpoint`, the runtime:

1. Creates an accordion list element.
2. Moves each trigger-content pair into accordion item wrappers.
3. Replaces original positions with comment placeholders.
4. Changes ARIA roles (`tab` → `button`, `tabpanel` → `region`).
5. Adds indicators if `showAccordionIndicator` is `true`.

When the viewport exceeds the breakpoint, everything is restored to the original tabs DOM.

#### Accessibility

The root component generates unique IDs for each trigger-content pair and wires up ARIA attributes automatically:

- **TabsList**: `role="tablist"`, `aria-orientation` synced to the `orientation` setting by the runtime.
- **TabsTrigger**: `role="tab"`, `aria-selected`, `aria-controls` pointing to its content panel, `tabIndex` (`0` when active, `-1` when inactive).
- **TabsContent**: `role="tabpanel"`, `aria-labelledby` pointing to its trigger, `tabIndex="0"`.
- **Disabled triggers**: `aria-disabled="true"`, `tabIndex="-1"`.
- In accordion mode, triggers use `role="button"` and `aria-expanded`; content panels use `role="region"`.

---

### TabsList

The `TabsList` is the container for all `TabsTrigger` elements. It receives `role="tablist"` and manages focus roving for its child triggers. Place it as a direct child of the root `Tabs` component.

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-tabs-list-default">
    CSS class applied to the list element.
  </Prop>
</PropsTable>

---

### TabsTrigger

The interactive button that activates its paired content panel. When the slot is empty, the trigger renders its `content.label` value as text. Triggers are paired with content panels by position — the first trigger in `TabsList` activates the first `TabsContent` child of `Tabs`.

<PropsTable group="Settings">
  <Prop name="disabled" type="boolean" defaultValue="false">
    Disables this trigger independently of the root `disabled` setting. A disabled trigger cannot receive focus or become active, and receives `aria-disabled="true"`.
  </Prop>
</PropsTable>

<PropsTable group="Content">
  <Prop name="label" type="string" defaultValue='""'>
    Fallback label text. Used when the trigger slot is empty.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-tabs-trigger-default">
    CSS class applied to the trigger element.
  </Prop>
</PropsTable>

#### Trigger Slot Content

When the trigger slot is populated, the slot content replaces the fallback label. Use this for rich trigger content — icons, badges, or any markup beyond plain text:

```
<TabsTrigger>
  {#slot default}
    ⚙ Settings
  {/slot}
</TabsTrigger>
```

---

### TabsContent

The content panel that shows or hides based on the active trigger. Only one panel is visible at a time — Tabs always keeps exactly one panel active. Place content panels as direct children of the root `Tabs` component, as siblings to `TabsList`.

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-tabs-content-default">
    CSS class applied to the content panel.
  </Prop>
</PropsTable>

---

## CSS Custom Properties

The default tab styles reference these CSS custom properties. Override them to customize appearance without replacing the default classes:

| Variable | Used For | Fallback |
|---|---|---|
| `--space-s` | Gap between list and content panels. | `1rem` |
| `--btn-padding-block` | Trigger padding (block axis). | — |
| `--btn-padding-inline` | Trigger padding (inline axis). | — |
| `--border-color-dark` | Inactive trigger border color. | — |
| `--text-dark-muted` | Inactive trigger text color. | — |
| `--primary` | Active trigger border color. | — |
| `--heading-color` | Active trigger text color. | — |
| `--ome-tabs-accordion-animation-duration` | Accordion animation duration (set inline from the `accordionAnimationDuration` prop). | — |

---

## Pattern Examples

### Vertical Tabbed Timeline

Used by the **Testimonial8** pattern — a vertical tabbed timeline with custom styled trigger cards:

- `orientation="vertical"`
- 4 triggers (Discovery, Onboarding, Building, Scaling) inside custom styled cards
- 4 content panels with testimonials
- Two-column grid layout (`0.35fr / 0.65fr`)

### Vertical Tabbed Features with Images

Used by the **FeatureSection20** pattern — vertical tabs with image content panels:

- `orientation="vertical"`
- 4 triggers as feature cards with icons
- 4 content panels showing images
- Two-column grid layout (`0.45fr / 0.55fr`)

Both patterns use vertical orientation and custom styling classes to override the default trigger appearance.

---

## Common Mistakes

<CommonMistake title="Nesting TabsContent inside TabsList">
  `TabsContent` panels must be **direct children of the root `Tabs` component**, not inside `TabsList`. The runtime pairs triggers and content by scanning `TabsList` children for triggers and `Tabs` children (excluding `TabsList`) for content panels. Placing content inside `TabsList` breaks pairing.
</CommonMistake>

<CommonMistake title="Mismatched trigger and content counts">
  If you have more triggers than content panels, the extra triggers are disabled. If you have more content panels than triggers, the extra panels stay hidden. Always ensure the counts match.
</CommonMistake>

<CommonMistake title="Expecting focus to activate tabs in accordion mode">
  In tabs mode, arrow keys move focus **and** activate the tab. In accordion mode (when responsive accordion is active), arrow keys only move focus — you must click or press `Enter`/`Space` to open a panel.
</CommonMistake>

<CommonMistake title="Expecting pairing by label or ID">
  Triggers and content panels are paired by **position only**. The second `TabsTrigger` always pairs with the second `TabsContent`, regardless of labels or any other attribute. Reordering one without the other breaks the pairing.
</CommonMistake>

---

## FAQs

<details>
<summary>Can I have nested tabs?</summary>

Yes. You can place a `Tabs` component inside a `TabsContent` panel. Each nested tabs instance manages its own state independently. Be mindful of the visual complexity — deeply nested tabs can be difficult to navigate.

</details>

<details>
<summary>How does the responsive accordion work?</summary>

When `enableResponsiveAccordion` is `true` and the viewport drops below `accordionBreakpoint`, the runtime restructures the DOM into accordion items. Triggers become accordion headers with `role="button"` and `aria-expanded`, and content panels become regions. When the viewport exceeds the breakpoint, the original tabs DOM is restored. The transition is seamless — no page reload required.

</details>

<details>
<summary>What if I need all panels collapsed?</summary>

Tabs always keeps exactly one panel active. If you need the ability to collapse all panels, use the Accordion component with `type="single"` instead.

</details>

<details>
<summary>Can I change the default active tab?</summary>

Yes. Use the `defaultIndex` setting on the root `Tabs` component. It is zero-based — `"0"` activates the first tab, `"1"` the second, and so on. If the index is invalid or points to a disabled trigger, it falls back to the first enabled trigger.

</details>

<details>
<summary>How do vertical tabs work with keyboard navigation?</summary>

In vertical orientation, `ArrowUp` and `ArrowDown` navigate between triggers instead of `ArrowLeft` and `ArrowRight`. `Home` and `End` still jump to the first and last enabled triggers. The arrow keys auto-activate the focused tab, same as horizontal mode.

</details>
