Tabs
Disclosure & Layoutimport { Tabs } from "bits-ui" Release channels
Automatic horizontal tabs with matched trigger and panel values
2.19.0
RecommendedProduction-ready APIs with documented migration paths.
2.20-next
WeeklyEarly component work for teams testing upcoming changes.
1.8.0
Security onlyMaintained for critical fixes while applications migrate.
<script lang="ts">
import { Tabs } from 'bits-ui';
const releases = {
stable: { version: '2.19.0', cadence: 'Recommended', detail: 'Production-ready APIs with documented migration paths.' },
preview: { version: '2.20-next', cadence: 'Weekly', detail: 'Early component work for teams testing upcoming changes.' },
legacy: { version: '1.8.0', cadence: 'Security only', detail: 'Maintained for critical fixes while applications migrate.' }
};
</script>
<Tabs.Root value="stable" class="mx-auto w-full max-w-xl">
<Tabs.List class="grid grid-cols-3 rounded-lg border border-ink-800 bg-ink-900 p-1">
{#each Object.keys(releases) as key (key)}
<Tabs.Trigger value={key} class="rounded-md px-3 py-2 text-sm font-medium capitalize text-ink-400 hover:text-ink-50 data-[state=active]:bg-ink-800 data-[state=active]:text-brand-400">
{key}
</Tabs.Trigger>
{/each}
</Tabs.List>
{#each Object.entries(releases) as [key, release] (key)}
<Tabs.Content value={key} class="mt-3 rounded-xl border border-ink-800 bg-ink-900 p-5">
<div class="flex flex-wrap items-center justify-between gap-2">
<h3 class="font-mono text-lg font-semibold text-ink-50">{release.version}</h3>
<span class="chip">{release.cadence}</span>
</div>
<p class="muted mt-3 text-sm leading-relaxed">{release.detail}</p>
</Tabs.Content>
{/each}
</Tabs.Root>
Vertical, manual activation
Arrow keys move focus; Enter or Space commits the focused tab
Appearance
Use the system theme and compact density on this device.
Notifications
Route deployment alerts to the on-call channel.
Security
Require a hardware key for administrative changes.
manual · use ↑/↓, then Enter or Space
<script lang="ts">
import { Tabs } from 'bits-ui';
import { Bell, LockKeyhole, Palette } from '@lucide/svelte';
let value = $state('appearance');
const items = [
{ value: 'appearance', label: 'Appearance', icon: Palette },
{ value: 'notifications', label: 'Notifications', icon: Bell },
{ value: 'security', label: 'Security', icon: LockKeyhole }
];
</script>
<Tabs.Root orientation="vertical" activationMode="manual" bind:value class="grid min-h-56 grid-cols-[11rem_1fr] overflow-hidden rounded-xl border border-ink-800 bg-ink-900">
<Tabs.List class="grid content-start gap-1 border-r border-ink-800 p-2">
{#each items as item (item.value)}
<Tabs.Trigger value={item.value} class="flex items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-ink-400 hover:bg-ink-850 hover:text-ink-50 data-[state=active]:bg-ink-800 data-[state=active]:text-brand-400">
<item.icon class="size-4" />{item.label}
</Tabs.Trigger>
{/each}
</Tabs.List>
<div class="p-5">
<Tabs.Content value="appearance">
<h3 class="font-semibold">Appearance</h3><p class="muted mt-2 text-sm">Use the system theme and compact density on this device.</p>
</Tabs.Content>
<Tabs.Content value="notifications">
<h3 class="font-semibold">Notifications</h3><p class="muted mt-2 text-sm">Route deployment alerts to the on-call channel.</p>
</Tabs.Content>
<Tabs.Content value="security">
<h3 class="font-semibold">Security</h3><p class="muted mt-2 text-sm">Require a hardware key for administrative changes.</p>
</Tabs.Content>
<p class="mt-6 font-mono text-xs text-ink-600">manual · use ↑/↓, then Enter or Space</p>
</div>
</Tabs.Root>
Every Trigger and Content requires a matching string value. Orientation also changes keyboard navigation: horizontal lists use Left/Right, while vertical lists use Up/Down.
Activation defaults to automatic, which selects a tab as focus moves. Set activationMode="manual" when focus movement should preview nothing until the user presses the trigger; keyboard navigation loops by default.