Navigation

Tabs

Root tabs composite that owns the active value and orientation and coordinates tab, tab-list, and tab-panel descendants via context.

import { Tabs } from '@lostgradient/cinder/tabs';
navigationtabs
01

Overview

Composite tabs root that coordinates tab list, tab triggers, and their content panels.

Usage

Tabs is a compound component. Import the parent once and compose its leaves via the namespace API: Tabs.List, Tabs.Trigger, and Tabs.Panel.

svelte
<script lang="ts">
  import { Tabs } from '@lostgradient/cinder/tabs';

  let active = $state('overview');
</script>

<Tabs bind:value={active}>
  <Tabs.List label="Project sections">
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="activity">Activity</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>

  <Tabs.Panel value="overview">Project overview.</Tabs.Panel>
  <Tabs.Panel value="activity">Recent activity.</Tabs.Panel>
  <Tabs.Panel value="settings">Project settings.</Tabs.Panel>
</Tabs>

The leaves remain importable individually for à-la-carte builds — see @lostgradient/cinder/tab, @lostgradient/cinder/tab-list, and @lostgradient/cinder/tab-panel.

Caller-owned panels

Use Tabs.Panel for ordinary tabs where each trigger owns one rendered panel. When the selected value controls one stable external surface, pass that panel id through Tabs.Trigger's controls prop and render the panel yourself:

svelte
<script lang="ts">
  import { Tabs } from '@lostgradient/cinder/tabs';

  let active = $state('index.ts');
  const tabIds: Record<string, string> = {
    'index.ts': 'editor-tab-index',
    'app.svelte': 'editor-tab-app',
  };
</script>

<Tabs bind:value={active}>
  <Tabs.List label="Editor files">
    <Tabs.Trigger id={tabIds['index.ts']} value="index.ts" controls="editor-panel">
      index.ts
    </Tabs.Trigger>
    <Tabs.Trigger id={tabIds['app.svelte']} value="app.svelte" controls="editor-panel">
      app.svelte
    </Tabs.Trigger>
  </Tabs.List>
</Tabs>

<div id="editor-panel" role="tabpanel" aria-labelledby={tabIds[active]} tabindex="0">
  {active}
</div>

This keeps Cinder in charge of roving tabindex, activation, disabled state, and tab styling while the caller owns an imperative or stateful panel element. Give the tabs explicit ids so the external panel can label itself with the active tab.

Trailing badges and counts

Each Tabs.Trigger accepts a trailing snippet for badges, counts, or status dots. Render the library Badge inside it for idiomatic styling — there is no separate badge class to learn:

svelte
<script lang="ts">
  import { Badge } from '@lostgradient/cinder/badge';
</script>

<Tabs.Trigger value="inbox">
  Inbox
  {#snippet trailing()}
    <Badge>3</Badge>
  {/snippet}
</Tabs.Trigger>

The trailing wrapper is aria-hidden, so its content is omitted from the tab's accessible name. Any count that carries meaning must therefore also live in the tab's visible children (the accessible name) — never rely on the trailing badge alone to convey it.

Live preview
02

When to use

Use when
  • Switching between several panels of related content under one heading area.
  • Building the WAI-ARIA tabs pattern with shared keyboard navigation and activation rules.
Avoid when
  • Picking one of two to five short values inline — use segmented-control instead.
  • Showing ordered progress through a wizard — use steps instead.
03

Examples

Basic tabs

Three tabs with associated panels; clicking a tab switches the panel. Uses the Tabs namespace API.

Fill pane tabs

Tabs can fill a bounded flex pane so the active panel takes the remaining height.

Vertical tabs with keyboard navigation

A vertical tablist: focus a tab and use ↑/↓ to move. The focus ring must render complete on all sides even though the tab list is a clipping (overflow) container.

Tabs with trailing badges

Tabs use a decorative `trailing` snippet (wrapped in aria-hidden) for badges and counters. The accessible name only includes the visible label.

Tabs with keyboard navigation

Focus a tab and use ←/→ to move; horizontal tablists activate on focus per the WAI-ARIA pattern.

04

Props

Props for tabs
Name Type Default Required Bindable Description
value text '' bind Bound active tab value.
onValueChangeRequest (next: string) => stringvoid Intercept a proposed tab value before the bindable value is written. Return a replacement value to transform it.
onValueChange (next: string) => void Notify after the bindable value has been committed.
orientation 'horizontal' | 'vertical' 'horizontal' Layout orientation. Affects which arrow keys move between tabs.
fill boolean false Fill the available space when Tabs is placed inside a bounded flex parent, letting the active TabPanel occupy the remaining height.
activateOnFocus boolean When true (default for horizontal), focusing a tab also activates it (the panel updates immediately). Vertical defaults to manual activation — the user moves focus with arrows, then presses Enter or Space.
children snippet req Tab and TabPanel children.