Number Field
A numeric input with steppers, locale-aware formatting and drag-to-change.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/number-field.jsonUsage
import {
NumberField, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement,
NumberFieldInput, NumberFieldScrubArea, NumberFieldUnit,
} from '@/components/ui/number-field'<NumberField defaultValue={5} min={0} max={40} step={0.5}>
<NumberFieldGroup>
<NumberFieldInput aria-label="Contrast dose" />
<NumberFieldUnit>mL</NumberFieldUnit>
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>Reach for this over <Input type="number" /> when the value has a step a user will want to
nudge — a dose, a slice thickness, a kVp. For a bare quantity with no natural increment, an
Input is less furniture.
Composition
<NumberField> {/* owns the value, min/max/step, formatting */}
<NumberFieldGroup> {/* the bordered shell — border, height, focus ring */}
<NumberFieldInput />
<NumberFieldUnit>mL</NumberFieldUnit>
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>The focus ring is drawn on the group rather than the input, because the steppers live inside the same box; a ring on the input alone would draw a line straight through them.
NumberFieldUnit is ours, not Base UI's — a plain aria-hidden span, because the unit is
already in the label and repeating it is noise for a screen reader.
Size
Heights 32, 40 and 48, the same Figma field scale as Input, so a number field and a text field on one row line up. The size goes on the root, in the same place as Input's.
Formatting
format is an Intl.NumberFormatOptions object, applied on blur and reversed on focus, so the
user always edits the raw number and always reads the formatted one. style: 'percent' means
the underlying value is a fraction — 0.92 displays as 92%, and step is in fractions too.
smallStep (alt, default 0.1) and largeStep (shift, default 10) come free with the
keyboard.
Drag to change
NumberFieldScrubArea turns any element into a horizontal drag handle for the value — a label
is the usual choice. It is a shortcut, never the only way in: the input is still typeable and
the arrow keys still work.
Inside a Field
Protocol range is 80–140 kVp.
data-invalid reaches the group from the surrounding Field, so
protocol limits can be expressed as min and max and reported with two different sentences by
match="rangeUnderflow" and match="rangeOverflow".
Stepping always clamps to min/max. Typing does not, if you pass allowOutOfRange — which
is what makes range validation possible at all, since a value that cannot leave the range can
never be out of it.
API Reference
Everything Base UI's Number Field accepts, plus:
Prop
Type
The ones you will reach for on the root:
Prop
Type