Keyboard
| Key | Effect |
|---|---|
| Arrow ↓ / ↑ | Move active item. |
| Home / End | First / last item. |
| Type-ahead | Jump to item by initial char. |
| Enter | Activate item. |
/ menu
@kumiki/headless/menu Svelte 5 attachments for Menu — trigger / menu / item compound with APG keyboard handling, dismissable wiring, activatedId-driven onSelect.
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
APG menu / menubar (open + highlighted item navigation).
@kumiki/machines/menuclosedclosed,openpnpm add @kumiki/machinesimport { createMenuMachine } from '@kumiki/machines/menu';
const m = createMenuMachine({ items: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] });
m.send({ type: 'OPEN' });
console.log(m.context.highlightedId); // 'a'
m.send({ type: 'NAVIGATE', direction: 'next' });
console.log(m.context.highlightedId); // 'b'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 { createMenu } from '@kumiki/headless/menu';
const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
const m = createMenu({ items, onSelect: (it) => console.log(it.id) });
</script>
<button {@attach m.trigger}>Open</button>
<div {@attach m.menu}>
{#each items as item (item.id)}
<div {@attach m.item(item)}>{item.label}</div>
{/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/menu<script lang="ts">
import { Root, Trigger, Menu, Item, Separator, type MenuItem } from '@kumiki/components/menu';
const items: MenuItem[] = [
{ id: 'new', label: 'New file' },
{ id: 'open', label: 'Open' },
{ id: 'sep1', label: '', kind: 'separator' },
{ id: 'export', label: 'Export' },
];
</script>
<Root {items} onSelect={(it) => console.log('selected', it.id)}>
{#snippet children({ items })}
<Trigger>Actions</Trigger>
<Menu>
{#each items as item (item.id)}
{#if item.kind === 'separator'}
<Separator {item} />
{:else}
<Item {item}>{item.label}</Item>
{/if}
{/each}
</Menu>
{/snippet}
</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 Menu } from '@kumiki/atelier/menu';
import type { MenuItem } from '@kumiki/components/menu';
const items: MenuItem[] = [
{ id: 'save', label: 'Save' },
{ id: 'duplicate', label: 'Duplicate' },
{ id: 'sep', label: '', kind: 'separator' },
{ id: 'delete', label: 'Delete' },
];
</script>
<Menu.Root {items} onSelect={(item) => console.log(item.id)}>
{#snippet children({ items: live })}
<Menu.Trigger>Actions ▾</Menu.Trigger>
<Menu.Menu>
{#each live as item (item.id)}
{#if item.kind === 'separator'}
<Menu.Separator {item} />
{:else}
<Menu.Item {item}>{item.label}</Menu.Item>
{/if}
{/each}
</Menu.Menu>
{/snippet}
</Menu.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Arrow ↓ / ↑ | Move active item. |
| Home / End | First / last item. |
| Type-ahead | Jump to item by initial char. |
| Enter | Activate item. |