Keyboard
| Key | Effect |
|---|---|
| Arrow ↑ / ↓ | Move focus to previous / next trigger. |
| Home / End | First / last trigger. |
| Space, Enter | Toggle the focused panel. |
| Tab | Move focus out of the accordion. |
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
Single- or multi-region disclosure (APG accordion).
pnpm add @kumiki/machinesimport { createAccordionMachine } from '@kumiki/machines/accordion';
const m = createAccordionMachine({
items: [
{ id: 'a', value: 'general' },
{ id: 'b', value: 'team' },
],
mode: 'single',
});
m.send({ type: 'TOGGLE', id: 'a' });
m.send({ type: 'TOGGLE', id: 'b' });
console.log(m.context.expandedIds); // ['b'] — single mode closes prior
m.send({ type: 'SET.MODE', mode: 'multiple' });
m.send({ type: 'TOGGLE', id: 'a' });
console.log(m.context.expandedIds); // ['b', 'a']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 { createAccordion } from '@kumiki/headless/accordion';
const c = createAccordion({ items });
</script>
<div {@attach c.root}>
{#each items as it (it.id)}
<section {@attach c.item(it)}>
<button {@attach c.trigger(it)}>{it.label}</button>
<div {@attach c.panel(it)}>…panel content…</div>
</section>
{/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/accordion<script lang="ts">
import { Root, Item, Trigger, Panel, type AccordionItem } from '@kumiki/components/accordion';
const items: AccordionItem<string>[] = [
{ id: 'q1', value: 'q1', label: 'How do I cancel?' },
{ id: 'q2', value: 'q2', label: 'When am I charged?' },
{ id: 'q3', value: 'q3', label: 'Refund policy?' },
];
let value = $state<string | null>(null);
</script>
<Root {items} bind:value>
{#each items as item (item.id)}
<Item value={item}>
<Trigger value={item}>{item.label}</Trigger>
<Panel value={item}>…answer…</Panel>
</Item>
{/each}
</Root><!-- multiple: any subset can be open simultaneously. -->
<Root {items} bind:value mode="multiple">
…
</Root>
<!-- single + collapsible=false: at least one must always be open. -->
<Root {items} bind:value collapsible={false}>
…
</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 Accordion } from '@kumiki/atelier/accordion';
import type { AccordionItem } from '@kumiki/components/accordion';
type Q = { question: string; answer: string };
const items: AccordionItem<Q>[] = [
{ id: 'q1', value: { question: 'Why Svelte 5?', answer: '…' } },
{ id: 'q2', value: { question: 'How are styles split?', answer: '…' } },
];
</script>
<Accordion.Root {items} mode="single" collapsible>
{#each items as q (q.id)}
<Accordion.Item value={q}>
<Accordion.Trigger value={q}>{q.value.question}</Accordion.Trigger>
<Accordion.Panel value={q}>{q.value.answer}</Accordion.Panel>
</Accordion.Item>
{/each}
</Accordion.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Arrow ↑ / ↓ | Move focus to previous / next trigger. |
| Home / End | First / last trigger. |
| Space, Enter | Toggle the focused panel. |
| Tab | Move focus out of the accordion. |