VI Controls

Star on GitHubFeedback
VI ControlsNative HTML and a hookot-number

Number input

A number on a baseline, yours in italic and formatted; less and more step it, and each step turns the changed figures over like a counter. Or it stands over a small ruler you can drag.

Report an issue with Number input

Example

Up to twelve at the long table.

Empty
Rest
Focus
At the end
Error

Print at least 50.

Disabled
scale, rest
scale, focus

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, button, and field along.

npx shadcn@latest add https://thedirectors.agency/ui/r/number-input.json

Usage

number-input-example.tsx
"use client"import * as React from "react"import { Field } from "@/components/ui/field"import { NumberInput } from "@/components/ui/number-input"export default function Example() {const [fee, setFee] = React.useState<number | null>(1250)return (<div className="grid w-full max-w-md gap-(--ot-space-8)"><Field label="Guests" hint="Up to twelve at the long table."><NumberInput defaultValue={4} min={1} max={12} unit={{ one: "guest", other: "guests" }} name="guests" /></Field><Field label="Fee for the second round" error={fee !== null && fee < 400 ? "Raise it to at least €400; the brief asks for two rounds of marks." : undefined}><NumberInput value={fee} onValueChange={setFee} min={0} step={50} format={{ style: "currency", currency: "EUR", maximumFractionDigits: 0 }} /></Field><Field label="Line height"><NumberInput variant="scale" defaultValue={1.45} min={1} max={2} step={0.01} /></Field></div>)}

Contract

  • Underneath: a native text input with role="spinbutton", plus a hook.
  • Anatomy: <div class="ot-number" data-variant="line | scale"> holding .ot-number-reading (.ot-number-cell, which stacks the input.ot-number-input and .ot-number-figures of .ot-number-figure spans, aria-hidden, in one grid cell; then an optional .ot-number-unit), a bracket ot-btn.ot-number-less and .ot-number-more (tabIndex -1), for scale .ot-number-tape (aria-hidden, style="--at", the value in steps), and with name a hidden input that submits the plain number. data-editing while you type, data-held while the ruler is dragged.
  • A field's baseline, and the accent line grows from where you touched it. Yours: the figure, in italic tabular figures, formatted by Intl.NumberFormat (format, locale, fixed to en so the first paint agrees). What you see is the figures layer; the input underneath holds the plain figures (the locale's decimal mark, no grouping), and a touch or a keystroke shows them to edit, with the caret in the accent. Leaving, Enter or a step reads what you typed (locale digits and decimal marks are normalized; grouping, currency and spaces are ignored), clamps it to min and max, and shows it formatted again; Escape puts back the value. Something that isn't a number goes back to the last number.
  • unit is ours, upright in pencil after the figure, and follows it (field-sizing: content), so the line reads as one phrase; forms by plural category agree with the number (1 guest, 3 guests), and screen readers hear it in aria-valuetext.
  • Line (the default): less and more stand in brackets at the line's end, the words of "Less is more."; at an end the word that can't go further goes pencil and is disabled. Each step turns the figures over like a counter's wheels: if the number keeps its length only the figures that changed turn, the units first and each place one --ot-arpeggio later, up as it grows and down as it shrinks; otherwise it turns over whole. Steps count from min (or zero) in whole steps, so 0.1 + 0.2 lands on 0.3.
  • Scale: the dial's tuner drawn small, and Paul Rand's dimension ticks. Your figure is set at --ot-f over the index, less and more at the two ends of its line, and the baseline is the edge of a ruler whose ticks hang from it, one per step (--ot-number-pitch, 10px) and a longer one every ten, fading at both ends. The index is an ink stroke standing through the ruler; focus draws the accent along its edge. Stepping slides the ruler a tick (moderato, spiccato); drag it sideways and it follows the hand, a step a tick, and the figures change without turning while it's held. The row always reads left to right, as the tumbler's figures do.
  • States: empty (the placeholder in our voice, in pencil), rest, focus, typing, at an end, error (aria-invalid, from a Field: the line crimson), disabled (the dotted baseline, pencil figures). Forced colours restate the tokens in system colours.
  • Keyboard: Up and Down step, Page Up and Page Down step ten, Home and End go to min and max, Enter commits, Escape puts back. Less and more are for the hand and keep focus where it was.

Props

Props of Number input
PropTypeDefaultWhat it does
variant"line" | "scale""line"line: your figure on a field's baseline, with less and more in brackets at the line's end, the words of "Less is more.". scale: the dial's tuner drawn small, after Paul Rand's dimension ticks; your figure stands over an index, and the baseline is the edge of a ruler whose ticks hang from it, one per step and a longer one every ten. Stepping slides the ruler a tick; drag it sideways and it follows the hand, the number with it.
value / defaultValuenumber | nullnullThe number, controlled or not. null is nothing written yet.
onValueChange(value: number | null) => voidCalled with each new number, as you type (once it reads as a number) and as you step.
min / maxnumberThe ends. Leaving the field clamps what you typed to them; less or more goes pencil at its end; Home and End go there.
stepnumber1One step, counted from min (or zero), so decimals land exactly: 0.1 + 0.2 is 0.3.
formatIntl.NumberFormatOptionsHow the number reads when you're not typing it: grouping, decimals, a currency or a unit. Typing shows the plain figures.
localestring"en"The locale that reads and writes the figures. Fixed by default, so the server and the browser print the same thing.
unitstring | { one, other, … }Ours, after your figure, upright in pencil. Forms by plural category agree with the number: 1 guest, 3 guests. Read out with the value.
namestringSubmits the plain number with a form, through a hidden input.
…input propsnativeid, placeholder, disabled, readOnly, aria-* and the rest go to the real input, a spinbutton. Inside a Field it takes the Field's id, hint and error.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Number input"Less is more."; the counter's wheelsLess and more in brackets at the line's end; the changed figures turn over
Scale number inputThe dial's tuner; Paul Rand dimension ticksYour figure over the index of a ruler that hangs from the baseline

How it moves

How it moves
ComponentArticulationWhat moves
Number inputCounterA step turns the changed figures over, units first, each place an arpeggio later
Scale number inputSlideA step slides the ruler a tick and lands with spiccato; dragged, it follows the hand