Number Field
A numeric input with steppers, a scrub-to-change label, keyboard stepping and Intl formatting. It is an Input underneath, so it sizes, focuses and joins an InputGroup like any other field.
Installation#
Usage#
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldIncrement,
NumberFieldDecrement,
} from "@/components/ui/number-field"
import { Field, FieldLabel } from "@/components/ui/field"
export function Example() {
return (
<Field>
<FieldLabel>Seats</FieldLabel>
<NumberField defaultValue={5} min={1} max={50}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</Field>
)
}Anatomy#
Every part but the group and the input is optional. Order the steppers however you like: trailing, or one on each side.
<NumberField>
<NumberFieldScrubArea>
<FieldLabel />
<NumberFieldScrubAreaCursor />
</NumberFieldScrubArea>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>Basic#
The value is a number, never a string. Type, press the arrow keys, or press the steppers. Hold a stepper and it repeats after a short pause.
Min, max and step#
Steps clamp to min and max, and a typed value clamps on blur. The stepper at a bound disables itself. Shift and PageUp/PageDown take largeStep, Alt takes smallStep, and Home/End jump to the bounds.
Formatting#
Pass Intl.NumberFormatOptions to format. Typed text is read back in the same locale, so 1.299,50 € and $1,299.50 both parse. A percent field stores the fraction: 25% is 0.25.
Scrub area#
Wrap the label in NumberFieldScrubArea and drag it. With a mouse the pointer locks, so the drag never runs off screen, and NumberFieldScrubAreaCursor draws the cursor in its place. Safari keeps the native resize cursor instead.
Quantity#
Put the steppers on either side and center the figure for a cart or a guest count.
Field#
Inside a Field, the label, the description and the error state wire themselves: FieldLabel names the input, FieldHint describes it, and hasError turns the frame destructive.
Input group#
NumberFieldGroup is an Input frame, so InputGroup strips it into a segment. Keep NumberField outside the group and the group's size reaches the field.
Sizes#
The same three heights as Input: sm (32 px), md (36 px) and lg (40 px), set on NumberFieldGroup. Unset, it follows the container (a Form imposes lg) or the density (sm under compact, md under comfortable). The steppers scale with it.
Disabled and read-only#
disabled dims the field and drops it from the form. readOnly keeps it focusable and readable but fixes the value, so the steppers and the keys do nothing.
Controlled#
onValueChange fires on every keystroke and step. onValueCommitted fires once the change settles, which is the one to save on.
API reference#
NumberField
The root. Owns the value, the steps and the formatting, and stacks a scrub-area label over the group. Reads id, disabled and required from a surrounding Field.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | null | - | The value, controlled. null is an empty field. |
| defaultValue | number | null | null | The initial value when uncontrolled. |
| onValueChange | (value: number | null) => void | - | Fires on every change: typing, stepping, scrubbing, the wheel. |
| onValueCommitted | (value: number | null) => void | - | Fires when a change settles: on blur, on Enter, after a key step, a press or a scrub. |
| min | number | - | The lowest value. Steps clamp to it, typed values clamp on blur, Home jumps to it. |
| max | number | - | The highest value. End jumps to it. |
| step | number | 1 | What the arrow keys and the steppers add. |
| smallStep | number | 0.1 | The step with Alt held. |
| largeStep | number | 10 | The step with Shift held, and for PageUp / PageDown. |
| format | Intl.NumberFormatOptions | - | How the value is shown: currency, percent, units, fraction digits. Typed text is parsed back with the same rules. |
| locale | string | - | The formatting locale. Unset, it is en-US on the server and the visitor's own after hydration. |
| disabled | boolean | false | Dims the group and ignores input. |
| readOnly | boolean | false | Keeps the value readable and focusable, but fixed. |
| required | boolean | false | Marks the input required. |
| name | string | - | Submits the raw number, not the formatted text, in a hidden input. |
| id | string | - | The input's id. Defaults to the Field's. |
| allowWheelScrub | boolean | false | Lets the mouse wheel step the value while the input has focus. |
| Data attribute | Description |
|---|---|
| data-disabled | Present when disabled. |
| data-readonly | Present when read-only. |
| data-required | Present when required. |
| data-scrubbing | Present while the scrub area is being dragged. |
NumberFieldGroup
The field frame. It is an InputRoot and keeps its input-root slot, so every Input prop and every InputGroup rule applies.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | - | 32, 36 or 40px. Unset, it follows the container, then the density. |
| density | "compact" | "comfortable" | - | Picks the default size: compact is sm, comfortable is md. |
| variant | "default" | "inline" | "default" | inline hides the frame until hover. |
| hasError | boolean | - | The destructive border and ring. Defaults to the Field's. |
| Data attribute | Description |
|---|---|
| data-scrubbing | Present while the scrub area is being dragged. |
NumberFieldInput
The text input, with role="spinbutton", aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext. Takes every input prop except the value ones.
NumberFieldIncrement
A small quiet button that steps up. Press and hold to repeat. It disables itself at max and stays out of the tab order, since the arrow keys do the same. Children replace the plus glyph.
NumberFieldDecrement
The same button, stepping down and disabling at min.
NumberFieldScrubArea
Wrap a label in it to drag the value. With a mouse it takes the pointer lock, so the drag never runs off screen.
| Prop | Type | Default | Description |
|---|---|---|---|
| direction | "horizontal" | "vertical" | "horizontal" | The drag axis. Right and up increase. |
| pixelSensitivity | number | 2 | Pixels of travel per step. Higher is slower. |
| Data attribute | Description |
|---|---|
| data-scrubbing | Present while dragging. |
NumberFieldScrubAreaCursor
The cursor shown while the pointer is locked, when the system cursor is hidden. Renders nothing otherwise. Children replace the arrows glyph.