Keyboard
| Key | Effect |
|---|---|
| Arrow ↑/↓ ←/→ | Move focus and select adjacent item. |
| Home / End | First / last item. |
| Tab | Move focus into / out of the group. |
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
APG radiogroup with roving tabindex.
pnpm add @kumiki/machinesimport { createRadioGroupMachine } from '@kumiki/machines/radio-group';
const m = createRadioGroupMachine({
items: [
{ id: '1', value: 'apple' },
{ id: '2', value: 'banana', disabled: true },
{ id: '3', value: 'cherry' },
],
});
m.send({ type: 'NAVIGATE', direction: 'first' });
console.log(m.context.value); // 'apple'
m.send({ type: 'NAVIGATE', direction: 'next' });
console.log(m.context.value); // 'cherry' (skipped disabled banana)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 { createRadioGroup } from '@kumiki/headless/radio-group';
const g = createRadioGroup({ items });
</script>
<div {@attach g.root}>
{#each items as item (item.id)}
<button {@attach g.item(item)}>{item.label}</button>
{/each}
</div>Compound components (<Root> / <Trigger> / …). Markup is fixed; styling is not. Same trade-off as a typical headless UI library.
pnpm add @kumiki/components/radio-group<script lang="ts">
import { Root, Item, type RadioItem } from '@kumiki/components/radio-group';
type Plan = 'free' | 'pro' | 'enterprise';
const plans: RadioItem<Plan>[] = [
{ id: 'free', value: 'free', label: 'Free' },
{ id: 'pro', value: 'pro', label: 'Pro' },
{ id: 'enterprise', value: 'enterprise', label: 'Enterprise' },
];
let value = $state<Plan | null>('free');
</script>
<Root items={plans} bind:value>
{#each plans as plan (plan.id)}
<Item value={plan}>{plan.label}</Item>
{/each}
</Root>const items = [
{ id: 'a', value: 'a', label: 'Option A' },
{ id: 'b', value: 'b', label: 'Option B', disabled: true },
{ id: 'c', value: 'c', label: 'Option C' },
];
// 'b' is unfocusable and unclickable; arrow-key nav skips it.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 RadioGroup } from '@kumiki/atelier/radio-group';
import type { RadioItem } from '@kumiki/components/radio-group';
type Plan = 'free' | 'pro' | 'team';
const plans: RadioItem<Plan>[] = [
{ id: 'free', value: 'free', label: 'Free' },
{ id: 'pro', value: 'pro', label: 'Pro' },
{ id: 'team', value: 'team', label: 'Team' },
];
let value = $state<Plan | null>('pro');
</script>
<RadioGroup.Root items={plans} bind:value aria-label="Plan">
{#each plans as p (p.id)}
<RadioGroup.Item value={p}>{p.label}</RadioGroup.Item>
{/each}
</RadioGroup.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Arrow ↑/↓ ←/→ | Move focus and select adjacent item. |
| Home / End | First / last item. |
| Tab | Move focus into / out of the group. |