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.
Per-field state (pristine / dirty / touched / validating / invalid).
pnpm add @kumiki/machinesimport { createFormFieldMachine } from '@kumiki/machines/form-field';
const m = createFormFieldMachine({ initialValue: '' });
m.send({ type: 'INPUT', value: 'a' });
m.send({ type: 'BLUR' });
const stale = m.context.validationToken;
// User keeps typing while validator is in flight.
m.send({ type: 'INPUT', value: 'ab' });
// The stale validation result arrives — DROPPED, token mismatch.
m.send({
type: 'VALIDATION_RESOLVE',
token: stale,
issues: [{ message: 'too short' }],
});
console.log(m.state); // 'editing' — not 'invalid'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 { createFormField } from '@kumiki/headless/form-field';
const f = createFormField({ initialValue: '', validator });
</script>
<label {@attach f.label}>Email</label>
<input {@attach f.input} type="email" />
<p {@attach f.description}>We won't share it.</p>
<div {@attach f.errors}></div>Compound components (<Root> / <Trigger> / …). Markup is fixed; styling is not. Same trade-off as a typical headless UI library.
pnpm add @kumiki/components/form-field<script lang="ts">
import * as Field from '@kumiki/components/form-field';
import * as v from 'valibot';
const emailSchema = v.pipe(v.string(), v.email('Enter a valid email'));
let value = $state('');
</script>
<Field.Root initialValue="" bind:value validator={emailSchema}>
<Field.Label>Email</Field.Label>
<Field.Input type="email" autocomplete="email" />
<Field.Description>We'll send a confirmation link.</Field.Description>
<Field.Errors />
</Field.Root><Field.Root initialValue="" validator={schema} validateOn="change">
<Field.Label>Username</Field.Label>
<Field.Input />
<Field.Errors />
</Field.Root>// Standard Schema validators may be async — Promise<Result> is fine.
// Race-token guarding is automatic: stale resolutions are dropped.
const usernameSchema: StandardSchemaV1<string, string> = {
'~standard': {
version: 1,
vendor: 'app',
validate: async (value) => {
const r = await fetch(`/api/users/${value}`);
if (r.status === 404) return { value: value as string };
return { issues: [{ message: 'Already taken' }] };
},
},
};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 Field } from '@kumiki/atelier/form-field';
import { z } from 'zod';
const validator = z.string().email('Enter a valid email');
let value = $state('');
</script>
<Field.Root initialValue="" bind:value name="email" {validator} validateOn="blur">
<Field.Label>Email</Field.Label>
<Field.Input type="email" autocomplete="email" />
<Field.Description>We never share your address.</Field.Description>
<Field.Errors />
</Field.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
No component-specific keymap. Inherits standard browser focus / activation behaviour.