Dial
A number you turn to. The arc sets a few on a half circle and rolls to your choice; the tuner is a long scale on the rim of a wheel you turn by hand, by scroll or by key, and your number reads above it in italic. Dynamics sets the number at the size of its loudness, from niente to fff; the tumbler sets a whole number figure by figure, like the wheels of a lock.
Report an issue with DialExample
Rest
Focus
Disabled
Dynamics, silent
Dynamics, loudest
Dynamics, focus
Tumbler
Tumbler, focus
Tumbler, disabled
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/dial.jsonUsage
import { Dial } from "@/components/ui/dial"export default function Example() {return (<div className="flex flex-wrap items-end justify-center gap-x-[var(--ot-space-8)] gap-y-[var(--ot-space-7)]"><Dial name="weeks" legend="How many weeks do we have?" options={[2, 4, 6, 8, 12, 16, 24]} defaultValue={8} unit="weeks" /><Dial variant="tuner" name="tempo" legend="Set the tempo" min={40} max={208} defaultValue={96} unit="bpm" /><Dial variant="dynamics" name="volume" legend="Volume" min={0} max={100} defaultValue={60} /><Dial variant="tumbler" name="year" legend="The year it was printed" min={1450} max={2026} defaultValue={1623} /></div>)}Contract
- Underneath: the arc is native radios with no script; the tuner and dynamics are a native range plus a hook; the tumbler is a native number input (
role="spinbutton") plus a hook. - Arc anatomy:
<fieldset class="ot-dial" data-variant="arc" style="--n: 7">, a legend, then.ot-dial-faceholding one<label style="--i: 0…n-1" data-text="N"><input type="radio"><span>N</span></label>per option, then.ot-dial-arc,.ot-dial-dot, and an optional.ot-dial-unit. - Arc: options sit on an arc from 170° to 10°.
--selis a registered number, set by:has()rules (up to 9 options) and transitioned over--ot-andantewith--ot-spiccato, so the dial swings a touch past and settles. Every option derives its distance from it, so the whole dial rolls: the chosen number swells to 2.4× and cross-fades to italic, the others fade from ink toward pencil by distance, and the accent dot travels the inner arc. It works without script. Keyboard: native radio group. - Tuner anatomy:
<fieldset class="ot-dial" data-variant="tuner">, a legend, then.ot-dial-face(aria-hidden) holding.ot-dial-read(a.ot-dial-drumand an optional.ot-dial-unit) and.ot-dial-scale(a.ot-dial-tickper step,style="--v: i"anddata-kind="minor | half | major", a major one holding its numeral, then.ot-dial-index), then<input type="range" class="ot-sr">. The range is the control: it takes focus and keys, carries the value to screen readers (aria-valuetextwith the unit) and submits the form. - Tuner: for more numbers than fit in view, a long scale on the rim of a wheel seen edge on. The script keeps one position,
--posin steps, and writes it to the face each frame; CSS sets every tick at the angle (v − pos) /--kon a radius of--k×--ot-dial-pitch(15 × 13px, 10px under 520px), so ticks spread under the index and crowd, narrow and fade as the rim turns away. Only the ticks within 34 steps of the index exist; the rest are made as the wheel brings them round. The index is an ink stroke standing through the rim with the accent dot on top, the slider's hand. A numeral inks as it passes the index. - Yours: the tuner's reading above the scale, a large italic numeral on a drum that turns with the wheel like a counter (
data-aanddata-bare the two numbers in view,--fhow far it has turned between them), with the unit after it in roman. - Turning the tuner: drag it sideways and the value follows the wheel step by step; a flick coasts and lands on a step with one small swing past; past either end the rim gives a little under the hand and springs back. A tap turns it to the mark under the finger; a tap on a coasting wheel stops it. A sideways swipe always turns it; the plain scroll wheel only once it has focus, and at either end it hands the scroll back to the page (it marks the face
data-lenis-prevent-wheelwhile it claims a scroll, so a smooth scroller leaves it alone). While it's held (data-heldon the root) the reading lifts 5px and settles back with spiccato. Under reduced motion there is no coast, spring or roll: the wheel and the reading step straight to the value. - States: rest, hover (the arc's number inks), focus (the arc's number, or the tuner's face, in the accent outline), chosen, held (tuner), disabled (45% opacity;
disabledgoes on the fieldset). Tuner keyboard: arrows step, Page Up / Page Down jump a major mark, Home / End go to the ends. - Dynamics anatomy:
<fieldset class="ot-dial" data-variant="dynamics" style="--u; --chars">, a legend, then.ot-dial-face(aria-hidden) holding.ot-dial-loud(the numeral and an optional unit) and.ot-dial-markingwithdata-mark, then<input type="range" class="ot-sr">witharia-valuetextsuch as "60, mezzo forte". - Dynamics: the score's dynamics as a control. The type scale is already named for them; here the number is set at the size of its loudness.
--uis how far along the range it is, and--e = (9^u − 1) / 8bends it the way loudness is heard, so the numeral grows from--ot-pto--ot-fffslowly at first and then fast. Under it, in the expression italic and the accent, its marking: niente at nothing (in pencil), then ppp to fff. The face keeps the room of the loudest numeral, so the page never reflows as it swells. Drag up or right to get louder (260px spans the range), scroll once focused, or use the range's keys; the numeral follows the hand while held (data-held) and settles with spiccato after a key or a scroll. - Tumbler anatomy:
<fieldset class="ot-dial" data-variant="tumbler">, a legend, then.ot-dial-face(dir="ltr",aria-hidden) holding.ot-dial-figures(style="--at", one.ot-dial-figureper place withdata-k, anddata-leadon a leading zero, then.ot-dial-cursor) and an optional unit, then<input type="number" role="spinbutton" inputmode="none" class="ot-sr">, described by a hint. - Tumbler: a whole number set one figure at a time, like the wheels of a combination lock, with the figure you turn held in accent parentheses, as the poster writes 20(25). Whole numbers run from min (at least 0) to max; max decides how many figures there are, and leading zeros stand in pencil. The parentheses slide between figures (
--at, moderato, spiccato) and open a little while held. A figure that changes rolls in from below or above, the way it turned, units first and each place one arpeggio later. Keyboard: up and down turn the held figure by its place (so up on the tens adds ten, and it carries), left and right move the parentheses, a typed digit sets the figure and steps right, Home and End go to min and max. Pointer: tap a figure to hold it, drag up or down to turn it (a step every 24px); scroll once focused. Figures always read left to right; right to left, only the legend moves. - Forced colours: the parentheses and the marking use
Highlight. Reduced motion: the swell, the roll and the parentheses change without travel.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "arc" | "tuner" | "dynamics" | "tumbler" | "arc" | The arc: up to nine choices on a half circle, native radios, no script. The tuner: any number of steps on a wheel that turns. Dynamics: the numeral grows from the quiet size to the loudest as you drag, scroll or press the arrows, and its dynamic marking (niente, ppp to fff) reads under it in the accent. The tumbler: whole numbers set one figure at a time; up and down turn the figure held in parentheses, left and right move to another, and a typed digit sets it and steps on. |
name | string | The form field's name, so a form submits the choice. | |
legend | ReactNode | The question, as a small label over the dial. It names the range too. | |
options | (string | number | { value, label? })[] | The choices in order. The arc takes up to nine; the tuner any number. | |
min / max / step | number | 0 / 100 / 1 | Tuner without options, and dynamics: the scale runs from min to max by step. Tumbler: whole numbers from min (at least 0) to max; max sets how many figures there are, and the leading zeros stand in pencil. |
major | number | 10 | Tuner: a numbered tick every this many steps, and a half-length one halfway between. Page Up / Page Down jump by it. |
value / defaultValue | string | number | The choice. Leave both out and the arc starts with none chosen, the tuner and dynamics at their first step, the tumbler at min. | |
onValueChange | (value: string | number) => void | Called with each new choice; on the tuner, with every step the wheel passes under your hand. | |
unit | string | Small words for the number, such as weeks: under the arc, after the reading on the others. | |
disabled | boolean | false | The dial fades and won't turn. It disables the fieldset, so every radio or the range goes with it. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Dial | WOVE; Paul Rand dimension lines | Numbers on an arc; the dial rolls. Or a scale on the rim of a wheel you turn, your number read above it |
| Dial, dynamics | The score's dynamics, which name the type scale | The number is set at the size of its loudness, its marking under it |
| Dial, tumbler | The poster's 20(25); a combination lock's wheels | Figure by figure, the one you turn held in parentheses |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Dial | Spiccato | The dial swings a touch past its stop |
| Tuner dial | Coast | The wheel follows the hand, coasts after a flick and lands on a step with one swing past; the reading turns like a counter |
| Dial, dynamics | Crescendo | The numeral grows with the hand, slowly then fast, and its marking changes under it |
| Dial, tumbler | Tumbler | A changed figure rolls in the way it turned, units first; the parentheses slide to the next figure |