Keyboard
| Key | Effect |
|---|---|
| Arrow ↑ / ↓ | Increment / decrement by step. |
| PageUp / PageDown | Increment / decrement by pageStep. |
Pure state machines (FSMs). No DOM. Run in server, Vitest, Worker. Reach for these when you only want the behavior.
APG spinbutton — bounded numeric input with step & long-press repeat.
pnpm add @kumiki/machinesimport { createNumberFieldMachine } from '@kumiki/machines/number-field';
const m = createNumberFieldMachine({ min: 0, max: 10, step: 1 });
m.send({ type: 'INCREMENT' }); // seeds from min
console.log(m.context.value); // 1
m.send({ type: 'CLEAR' });
console.log(m.context.value); // nullSvelte 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 { createNumberField } from '@kumiki/headless/number-field';
const n = createNumberField({ min: 0, max: 10 });
</script>
<div {@attach n.root}>
<button {@attach n.decrement_}>−</button>
<input {@attach n.input} aria-label="Quantity" />
<button {@attach n.increment_}>+</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/number-field<script lang="ts">
import {
Root,
Input,
Increment,
Decrement,
} from '@kumiki/components/number-field';
let qty = $state(1);
</script>
<Root bind:value={qty} min={0} max={99} step={1}>
<Decrement aria-label="Decrease">−</Decrement>
<Input aria-label="Quantity" />
<Increment aria-label="Increase">+</Increment>
</Root><Root
bind:value={price}
min={0}
step={100}
format={(n) => '$' + n.toLocaleString('en-US')}
parse={(raw) => Number(raw.replace(/[$,\s]/g, '')) || undefined}
>
<Decrement>−</Decrement>
<Input aria-label="Price" />
<Increment>+</Increment>
</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 NumberField } from '@kumiki/atelier/number-field';
let qty = $state<number | null>(1);
</script>
<NumberField.Root min={0} max={99} step={1} bind:value={qty}>
<NumberField.Decrement aria-label="Decrease" />
<NumberField.Input aria-label="Quantity" />
<NumberField.Increment aria-label="Increase" />
</NumberField.Root>/ accessibility
axe-core — run on every PR (LTR + RTL × every documented state).
| Key | Effect |
|---|---|
| Arrow ↑ / ↓ | Increment / decrement by step. |
| PageUp / PageDown | Increment / decrement by pageStep. |