Keyboard
| Key | Effect |
|---|---|
| Escape | Close (when policy allows). |
| Tab / Shift+Tab | Cycle focus within trap. |
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
Modal / non-modal dialog with focus trap + scroll-lock states.
pnpm add @kumiki/machinesimport { createDialogMachine } from '@kumiki/machines/dialog';
const m = createDialogMachine({ closeOnEscape: false });
m.send({ type: 'OPEN' });
m.send({ type: 'ESCAPE' }); // ignored — policy says no
console.log(m.state); // 'open'
m.send({ type: 'SET.CLOSE_ON_ESCAPE', value: true });
m.send({ type: 'ESCAPE' });
console.log(m.state); // 'closed'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 { createDialog } from '@kumiki/headless/dialog';
const d = createDialog({ modal: true });
</script>
<button {@attach d.trigger}>Open</button>
<div {@attach d.overlay}></div>
<div {@attach d.content}>
<h2 {@attach d.title}>Title</h2>
<p {@attach d.description}>Description</p>
<button {@attach d.close}>Close</button>
</div>Compound components (<Root> / <Trigger> / …). Markup is fixed; styling is not. Same trade-off as a typical headless UI library.
pnpm add @kumiki/components/dialog<script lang="ts">
import * as Dialog from '@kumiki/components/dialog';
let open = $state(false);
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Confirm purchase</Dialog.Title>
<Dialog.Description>You're about to enroll in the Pro plan.</Dialog.Description>
<Dialog.Close>Cancel</Dialog.Close>
<Dialog.Close>Confirm</Dialog.Close>
</Dialog.Content>
</Dialog.Root><Dialog.Root bind:open modal={false}>
<Dialog.Trigger>Show</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Tips</Dialog.Title>
<p>Background remains interactive — useful for inline help panels.</p>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Root><!-- e.g. a destructive action that requires explicit confirmation. -->
<Dialog.Root bind:open closeOnEscape={false} closeOnOutsideClick={false}>
<Dialog.Trigger>Delete account</Dialog.Trigger>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>This cannot be undone.</Dialog.Description>
<Dialog.Close>Keep account</Dialog.Close>
<button onclick={destructive}>Yes, delete</button>
</Dialog.Content>
</Dialog.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 Dialog } from '@kumiki/atelier/dialog';
</script>
<Dialog.Root>
<Dialog.Trigger>Edit profile</Dialog.Trigger>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Edit profile</Dialog.Title>
<Dialog.Description>Update your account details.</Dialog.Description>
<!-- form goes here -->
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Root><script lang="ts">
import { Vanilla as Dialog } from '@kumiki/atelier/dialog';
</script>
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Hello</Dialog.Title>
<Dialog.Close />
</Dialog.Content>
</Dialog.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Escape | Close (when policy allows). |
| Tab / Shift+Tab | Cycle focus within trap. |