---
outline: deep
---

# Tabs

Tabs organize content into panels, showing one at a time. Progressive enhancement — plain HTML is enhanced with ARIA roles, keyboard navigation, and an animated indicator. Mark the initially-active tab with `aria-selected="true"` so it is styled as selected before the script loads.

**`<l-tabs>`** — Progressive Custom Element

## Options

### Enclosed variant

Add `variant="enclosed"` for a pill-shaped tablist with a sliding background indicator.

```html
<l-tabs variant="enclosed">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>
```

### Line variant

Add `variant="line"` for a tablist with a sliding underline indicator.

```html
<l-tabs variant="line">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>
```

Hovering a tab reveals a subtle pill behind its label, detached from the bottom border. Restyle it with `--hover-color` (and `--hover-inset` for the gap), along with the active underline and the static bottom border via `--indicator-color`, `--indicator-thickness`, `--track-color` and `--track-thickness`.

```html
<l-tabs
  variant="line"
  style="
    --indicator-color: var(--l-color-bg-fill-info-strong);
    --indicator-thickness: 3px;
    --track-color: var(--l-color-divider);
    --hover-color: var(--l-color-bg-fill-info-soft);
  "
>
  <div>
    <button
      name="overview"
      aria-selected="true"
    >
      Overview
    </button>
    <button name="analytics">Analytics</button>
    <button name="reports">Reports</button>
  </div>
  <div class="p-4 text-sm text-secondary">High-level overview.</div>
  <div class="p-4 text-sm text-secondary">Traffic and engagement analytics.</div>
  <div class="p-4 text-sm text-secondary">Exported reports.</div>
</l-tabs>
```

### Full width

Add `full-width` to stretch tabs across the container.

```html
<l-tabs
  variant="enclosed"
  full-width
>
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>
```

### Default active tab

Set `value="1"` to activate a specific tab on load (0-based index).

```html
<l-tabs
  variant="enclosed"
  value="1"
>
  <div>
    <button name="account">Account</button>
    <button
      name="password"
      aria-selected="true"
    >
      Password
    </button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>
```

### Disabled tab

Add `disabled` to a tab button to take it out of play. It is skipped by arrow keys, `Home` and `End`, it cannot be selected by a click or by `value`, and it never shows the hover pill. `aria-disabled="true"` behaves identically — reach for it only when the native attribute is out of reach (a framework binding, a non-`button` element).

`l-tabs` itself takes no `disabled` attribute. To freeze tab switching — while a page loads, say — put `inert` on the tablist `div`: the tabs leave the tab sequence and stop answering clicks, and the active panel stays reachable.

```html
<l-tabs variant="line">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button
      name="billing"
      disabled
    >
      Billing
    </button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Upgrade your plan to manage billing.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>
```

## Accessibility

### Criteria

- **Role** — First child div gets `role="tablist"`, buttons get `role="tab"`, remaining divs get `role="tabpanel"`
- **Linked controls** — Each tab has `aria-controls` pointing to its panel; each panel has `aria-labelledby` pointing back to its tab
- **Selection state** — Active tab has `aria-selected="true"`; inactive tabs have `aria-selected="false"`
- **Focus management** — Roving tabindex — active tab has `tabindex="0"`, others `tabindex="-1"`. Panels have `tabindex="0"` for keyboard access
- **Disabled tabs** — Tabs marked `disabled` or `aria-disabled="true"` are skipped by arrow keys, `Home` and `End`, and never take the roving `tabindex="0"` — so the tablist always keeps a reachable entry point
- **Hidden content** — Inactive panels use the `hidden` attribute
- **Motion** — Indicator animation respects `prefers-reduced-motion`

### Rules

- The first child of `l-tabs` must be a `div` containing `button` elements for the tab triggers
- Each remaining child `div` maps to a tab panel in order
- Do not add `role` or `aria-*` attributes manually — the custom element sets them automatically
- `l-tabs` has no host-level `disabled` attribute — put `inert` on the tablist `div` to freeze tab switching

### Keyboard interactions

- `ArrowRight` — Moves focus to the next enabled tab and activates it (horizontal orientation)
- `ArrowLeft` — Moves focus to the previous enabled tab and activates it (horizontal orientation)
- `ArrowDown` — Moves focus to the next enabled tab and activates it (vertical orientation)
- `ArrowUp` — Moves focus to the previous enabled tab and activates it (vertical orientation)
- `Home` — Moves focus to the first enabled tab and activates it
- `End` — Moves focus to the last enabled tab and activates it
- `Tab` — Moves focus out of the tablist to the active panel

## API reference

### Importing

```js
import 'luxen-ui/tabs';
```

```css
@import 'luxen-ui/css/tabs/enclosed';
```

```css
@import 'luxen-ui/css/tabs/line';
```

### Attributes & Properties

- **variant**: `TabsVariant` (default: `'line'`) — Visual variant.
- **value**: `string` (default: `'0'`) — Index of the active tab (0-based).
- **full-width**: `boolean` (default: `false`) — Stretch tabs to fill container width.
- **orientation**: `TabsOrientation` (default: `'horizontal'`) — Tab orientation.

### Events

- **change** — Fired when the active tab changes. Bubbles. Not cancelable. Properties: `index: number`, `name: string | null`.

### CSS custom properties

- `--indicator-color` (default: `var(--l-color-text-primary)`) — `line` variant: color of the active underline that slides under the selected tab.
- `--indicator-thickness` (default: `2px`) — `line` variant: thickness of the active underline.
- `--track-color` (default: `var(--l-color-border)`) — `line` variant: color of the static bottom border the tabs sit on.
- `--track-thickness` (default: `1px`) — `line` variant: thickness of the static bottom border.
- `--hover-color` (default: `var(--l-color-bg-state-hover)`) — `line` variant: fill of the pill that appears behind a hovered tab label.
- `--hover-inset` (default: `4px`) — `line` variant: block-axis gap between the hover pill and the tab edges, keeping it detached from the bottom border.
