Keyboard
No component-specific keymap. Inherits standard browser focus / activation behaviour.
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
APG listbox-style select (no inline filter — use combobox for that).
pnpm add @kumiki/machinesimport { createSelectMachine } from '@kumiki/machines/select';
const m = createSelectMachine({
items: [
{ id: '1', value: 'apple', label: 'Apple' },
{ id: '2', value: 'banana', label: 'Banana', disabled: true },
{ id: '3', value: 'cherry', label: 'Cherry' },
],
});
m.send({ type: 'OPEN' });
m.send({ type: 'NAVIGATE', direction: 'next' }); // skip banana → cherry
m.send({ type: 'SELECT', id: '3' });
console.log(m.context.value); // 'cherry'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 { createSelect } from '@kumiki/headless/select';
const s = createSelect({ items });
</script>
<button {@attach s.trigger}>{labelFor(s.value)}</button>
<ul {@attach s.listbox}>
{#each items as it (it.id)}
<li {@attach s.option(it)}>{it.label}</li>
{/each}
</ul>Compound components (<Root> / <Trigger> / …). Markup is fixed; styling is not. Same trade-off as a typical headless UI library.
pnpm add @kumiki/components/select<script lang="ts">
import { Root, Trigger, Listbox, Option, type SelectItem } from '@kumiki/components/select';
type Plan = 'free' | 'pro' | 'enterprise';
const plans: SelectItem<Plan>[] = [
{ id: 'free', value: 'free', label: 'Free' },
{ id: 'pro', value: 'pro', label: 'Pro' },
{ id: 'enterprise', value: 'enterprise', label: 'Enterprise' },
];
let value = $state<Plan | null>('pro');
</script>
<Root items={plans} bind:value>
<Trigger>{value ?? 'Pick a plan'}</Trigger>
<Listbox>
{#each plans as plan (plan.id)}
<Option value={plan}>{plan.label}</Option>
{/each}
</Listbox>
</Root><!-- 'banana' is unfocusable; arrow keys skip it. clamp stops at edges. -->
<Root items={items} bind:value navigation="clamp">
<Trigger>{labelFor(value) ?? 'Pick'}</Trigger>
<Listbox>{#each items as it (it.id)}<Option value={it}>{it.label}</Option>{/each}</Listbox>
</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 Select } from '@kumiki/atelier/select';
type Fruit = 'apple' | 'banana' | 'cherry';
const items = [
{ id: 'apple', value: 'apple', label: 'Apple' },
{ id: 'banana', value: 'banana', label: 'Banana' },
{ id: 'cherry', value: 'cherry', label: 'Cherry' },
];
let value = $state<Fruit | null>(null);
</script>
<Select.Root {items} bind:value>
<Select.Trigger>{value ?? 'Pick a fruit'}</Select.Trigger>
<Select.Listbox>
{#each items as it (it.id)}
<Select.Option value={it}>{it.label}</Select.Option>
{/each}
</Select.Listbox>
</Select.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
No component-specific keymap. Inherits standard browser focus / activation behaviour.