Form controls, machined. A switch is a toggle on a panel, a slider is a fader on its strip, a progress bar is a dial gauge. Eighteen accessible controls for React and any DOM, drawn as working isometric objects over native inputs. No dependencies.
Every control live, with its code: in-c0.github.io/knurl
npm i knurl-uiESM only. React 18 or later is an optional peer, needed only by knurl-ui/react.
Or add a control's source to your project with the shadcn CLI. It writes the control and the shared core to components/knurl:
npx shadcn@latest add https://in-c0.github.io/knurl/r/toggle.json
npx shadcn@latest add https://in-c0.github.io/knurl/r/knurl.json # all eighteenimport { Fader, Toggle } from "knurl-ui/react";
export function Mixer() {
const [gain, setGain] = useState(60);
return (
<>
<Toggle label="Mute" />
<Fader label="Gain" value={gain} onValueChange={setGain} />
</>
);
}A component renders one <div> at its own aspect ratio and draws on mount, so a server render reserves the space and nothing shifts. Pass value / checked to control it, or defaultValue / defaultChecked. It takes every <div> attribute and forwards its ref.
import { gauge } from "knurl-ui";
const g = gauge(document.querySelector("#upload")!, { label: "Uploading" });
g.update({ value: 42 });
g.destroy();update changes options on the running control (a key set to undefined goes back to its default); destroy removes only what the control added.
| Component | Stands for | Built as | Underneath |
|---|---|---|---|
Toggle |
Switch | A bat-handle toggle on a panel; a lamp lights when on. | <input type="checkbox" role="switch"> |
Rocker |
Checkbox | A panel-mount rocker switch, with a centre-off position for mixed. | <input type="checkbox"> |
Fader |
Slider | A mixing-desk fader over a ruler of its steps. | <input type="range"> |
Caliper |
Range slider | A vernier caliper whose two jaws mark the range. | <input type="range"> × 2 |
Knob |
Rotary slider | A knurled knob over a 270° scale. | <input type="range"> |
Joystick |
2D input | An arcade joystick that springs back to centre. | <input type="range"> × 2 |
Keycap |
Button | One mechanical key in a plate. | <button> |
Counter |
Number field | A tally counter whose drums roll like an odometer. | <input type="number"> |
Combo |
One-time code | A combination padlock that opens on the right digits. | <input inputmode="numeric"> |
Tray |
File drop zone | A desk letter tray that stacks a sheet per file. | <input type="file"> |
Presets |
Radio group | A bank of latching preset keys. | native radios |
Selector |
Select | A rotary selector switch with a chicken-head knob. | native radios |
Gauge |
Progress / meter | A dial gauge with a damped needle and a tell-tale hand. | role="progressbar" / "meter" |
Tank |
Level meter | A storage tank whose sight glass shows the level. | role="meter" / "progressbar" |
Track |
Step progress | A rail line where a handcar rolls from station to station. | role="progressbar" + list |
Timer |
Countdown | A wind-up kitchen timer whose bell shakes at zero. | role="timer" + range |
Annunciator |
Status panel | A panel of labelled lamps that light, warn and blink. | role="group" + list + live region |
Gears |
Loading status | A 24 : 16 : 12 gear train that eases up to speed. | role="status" |
Three whole interfaces built from several controls, live on the site with their code:
- Mono synth — a Web Audio voice: selector, knobs, joystick, keycaps, gauge and fader.
- Release console — a deploy train: rockers, a combination lock to arm it, track, gauge, gears, annunciator and timer.
- Intake desk — a document intake form: tray, caliper, presets, selector, counter, gauge, gears, annunciator and tank.
| Option | Type | Default | |
|---|---|---|---|
finish |
"line" | "solid" |
"line" |
Hairlines on plates, or four-tone shaded sides with lit lids and a contact shadow. Same geometry; the difference is CSS. |
theme |
"auto" | "light" | "dark" |
"auto" |
Follows .dark / data-theme="dark" on an ancestor, then color-scheme. |
label |
string |
English default | The accessible name. |
disabled |
boolean |
false |
|
name |
string |
Form field name: the value submits with the surrounding <form>. |
Value controls (Fader, Knob, Counter, Caliper) also take min, max, step, onValueChange, onValueCommit and format (for aria-valuetext). The per-control API is on the site.
.mixer [data-knurl] {
--knurl-plate: #101014; /* the colour the control sits on: plates are filled with it */
--knurl-hi: #fafafa; /* what is lit */
--knurl-edge: #a1a1aa; /* silhouettes */
--knurl-mid: #52525b; /* other strokes */
--knurl-lo: #27272a; /* what recedes */
--knurl-accent: #f76b15; /* the lamp, the lit slot, the red zone */
--knurl-stroke: 0.9; /* stroke width in CSS px, at any size */
}The solid finish adds --knurl-top, --knurl-s0 … --knurl-s3 and --knurl-shadow. Every rule sits inside :where(), so any rule of yours wins without !important.
- Accessibility. Each control is drawn over a visually hidden native input, button or ARIA role. Keyboard, screen readers and form submission are the platform's own; the focus ring follows the native element.
- Motion. Springs with real stiffness and damping. Under
prefers-reduced-motionevery spring lands in one step and the gears hold still. - Performance. One
requestAnimationFrameloop for every control on the page; a control offscreen or at rest does no work, and the loop stops when nothing moves. - Server rendering. Nothing touches
windowat import. The React entry is a client module.
Knurl stands on two MIT projects. Anatomy supplies the craft: invent the real object whose working is the idea, build it from many small precise parts, shade it in four tones. Hairline supplies the runtime: hairline figures that answer the pointer, a shared frame loop asleep offscreen, zero-specificity styles and a token theme. See LICENSE.
The docs site follows the patterns shared by the five fastest-growing component sites of the last year; the analysis is in docs/RESEARCH.md.
docs/MAKING-A-CONTROL.md is the brief every control was built to: the object, the parts, native semantics, motion and the checks.
pnpm install
pnpm dev # the site, against the source
pnpm test # unit tests (jsdom)
pnpm build # the package, to dist/
pnpm site # registry JSON + the site, to site/distMIT

