Keyboard
| Key | Effect |
|---|---|
| Arrow ←/→ | Move focus to adjacent tab. |
| Home / End | First / last tab. |
| Enter, Space | Activate the focused tab (manual mode). |
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
APG tabs — automatic / manual activation, RTL-aware arrow keys.
pnpm add @kumiki/machinesimport { createTabsMachine } from '@kumiki/machines/tabs';
const m = createTabsMachine({
items: [
{ id: 'a', value: 'account' },
{ id: 'b', value: 'billing', disabled: true },
{ id: 'c', value: 'team' },
],
activation: 'manual',
});
m.send({ type: 'FOCUS', id: 'c' });
console.log(m.context.value); // 'account' (manual: focus ≠ activate)
m.send({ type: 'ACTIVATE_FOCUSED' });
console.log(m.context.value); // 'team'Svelte 5 {@attach} factories. Glue ARIA / keyboard / focus onto any DOM node you choose. **Best when you want full control over markup and styling.**
pnpm add @kumiki/headless<script lang="ts">
import { createTabs } from '@kumiki/headless/tabs';
const c = createTabs({ items, orientation: 'horizontal' });
</script>
<div {@attach c.list}>
{#each items as it (it.id)}
<button {@attach c.tab(it)}>{it.label}</button>
{/each}
</div>
{#each items as it (it.id)}
<div {@attach c.panel(it)}>…content…</div>
{/each}Compound components (<Root> / <Trigger> / …). Markup is fixed; styling is not. Same trade-off as a typical headless UI library.
pnpm add @kumiki/components/tabs<script lang="ts">
import { Root, List, Tab, Panel, type TabItem } from '@kumiki/components/tabs';
const items: TabItem[] = [
{ id: 'account', value: 'account', label: 'Account' },
{ id: 'team', value: 'team', label: 'Team' },
{ id: 'security', value: 'security', label: 'Security' },
];
let value = $state<string | null>('account');
</script>
<Root {items} bind:value>
<List>
{#each items as item (item.id)}
<Tab value={item}>{item.label}</Tab>
{/each}
</List>
{#each items as item (item.id)}
<Panel value={item}>…panel content…</Panel>
{/each}
</Root><Root {items} bind:value activation="manual">
<!-- Arrow keys move focus only; Enter or Space activates. -->
<List>{#each items as it (it.id)}<Tab value={it}>{it.label}</Tab>{/each}</List>
{#each items as it (it.id)}<Panel value={it}>{it.label}</Panel>{/each}
</Root><!-- orientation switches which arrows navigate.
direction inverts horizontal arrows; vertical is dir-agnostic. -->
<Root {items} bind:value orientation="vertical" direction="rtl">
<List>{#each items as it (it.id)}<Tab value={it}>{it.label}</Tab>{/each}</List>
{#each items as it (it.id)}<Panel value={it}>{it.label}</Panel>{/each}
</Root>Styled, copy-paste presets (preview). Run pnpm kumiki add to drop the source into your project, then edit freely.
Live preview…
pnpm add @kumiki/atelier<script lang="ts">
import { Tailwind as Tabs } from '@kumiki/atelier/tabs';
import type { TabItem } from '@kumiki/components/tabs';
const items: TabItem[] = [
{ id: 'account', value: 'account', label: 'Account' },
{ id: 'team', value: 'team', label: 'Team' },
{ id: 'security', value: 'security', label: 'Security' },
];
</script>
<Tabs.Root {items} defaultValue="account">
<Tabs.List>
{#each items as t (t.id)}<Tabs.Tab value={t}>{t.label}</Tabs.Tab>{/each}
</Tabs.List>
<Tabs.Panel value={items[0]!}>…</Tabs.Panel>
<Tabs.Panel value={items[1]!}>…</Tabs.Panel>
<Tabs.Panel value={items[2]!}>…</Tabs.Panel>
</Tabs.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Arrow ←/→ | Move focus to adjacent tab. |
| Home / End | First / last tab. |
| Enter, Space | Activate the focused tab (manual mode). |