Number input
A numeric field with steppers, units, precision and locale formatting, that steps with the arrow keys and can be scrubbed by dragging its label.
npx prodular-ui add number-inputQuantity
Split steppers for small counts. Shift with an arrow key steps by largeStep.
$24 per seat, $288 a month. Shift and an arrow key steps by 10.
"use client"
import * as React from "react"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"import { NumberInput } from "@/components/ui/number-input"
const pricePerSeat = 24
export default function NumberInputSeats() { const [seats, setSeats] = React.useState<number | null>(12) const total = (seats ?? 0) * pricePerSeat
return ( <Field className="w-full max-w-60"> <FieldLabel>Seats</FieldLabel> <NumberInput value={seats} onValueChange={setSeats} min={1} max={500} largeStep={10} /> <FieldDescription className="tabular-nums"> ${pricePerSeat} per seat, ${total.toLocaleString("en-US")} a month. Shift and an arrow key steps by 10. </FieldDescription> </Field> )}Units, precision and locale
A unit sits outside the value. format sets precision, locale sets grouping and decimal marks, scrubLabel lets people drag to adjust.
Formatted for the German locale. Drag the Request timeout label to scrub its value.
"use client"
import * as React from "react"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"import { NumberInput } from "@/components/ui/number-input"
export default function NumberInputUnits() { const [discount, setDiscount] = React.useState<number | null>(12.5) const [timeoutMs, setTimeoutMs] = React.useState<number | null>(800) const [volume, setVolume] = React.useState<number | null>(1250000)
return ( <div className="grid w-full max-w-lg gap-6 sm:grid-cols-2"> <Field> <FieldLabel>Annual discount</FieldLabel> <NumberInput value={discount} onValueChange={setDiscount} min={0} max={40} step={0.5} format={{ maximumFractionDigits: 1 }} unit="%" steppers="end" /> </Field> <NumberInput scrubLabel="Request timeout" value={timeoutMs} onValueChange={setTimeoutMs} min={100} max={10000} step={50} unit="ms" steppers="none" aria-label="Request timeout" /> <Field className="sm:col-span-2"> <FieldLabel>Monthly transaction volume</FieldLabel> <NumberInput value={volume} onValueChange={setVolume} min={0} step={1000} locale="de-DE" steppers="none" unit="EUR" /> <FieldDescription>Formatted for the German locale. Drag the Request timeout label to scrub its value.</FieldDescription> </Field> </div> )}Sizes
Matches Input heights, plus the disabled state.
import { NumberInput } from "@/components/ui/number-input"
export default function NumberInputSizes() { return ( <div className="flex w-full max-w-56 flex-col gap-3"> <NumberInput size="sm" defaultValue={2} min={0} aria-label="Retries" /> <NumberInput defaultValue={4} min={0} aria-label="Workers" /> <NumberInput size="lg" defaultValue={8} min={0} aria-label="Replicas" /> <NumberInput defaultValue={3} disabled aria-label="Regions" /> </div> )}Anatomy
<Field> <FieldLabel>Seats</FieldLabel> <NumberInput value={seats} onValueChange={setSeats} min={1} max={500} /></Field>Guidance
Use it when
- Quantities, limits, percentages and durations: anything people type as a number and sometimes nudge up or down.
- steppers split for small counts people adjust by one, end for compact forms, none when values are usually typed.
- unit for what the number means (%, ms, seats). It is never part of the value.
Avoid
- Money. Use Amount input, which takes decimals and the symbol from the currency.
- Identifiers that only look numeric, like phone numbers, postcodes or card numbers. Use Input.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Always give the field a name people can read: wrap it in Field with FieldLabel, or pass aria-label.
- Set min and max whenever the backend has limits, and step to the real increment. Do not validate ranges yourself after the fact.
- Set precision with format (maximumFractionDigits), not by rounding in onValueChange.
- Use onValueChange for live updates and onValueCommitted to save, so a request is not sent on every keystroke.
- The value can be null when the field is empty. Handle it.
API
NumberInput
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onValueChange | number | null | Controlled or uncontrolled value. Null when empty. | |
| min / max | number | Clamp the value and disable the steppers at the limits. | |
| step / smallStep / largeStep | number | 1 / 0.1 / 10 | Arrow keys step by step, with Alt by smallStep and with Shift by largeStep. |
| format | Intl.NumberFormatOptions | Precision and style, for example { maximumFractionDigits: 1 }. | |
| locale | string | Grouping and decimal marks, for example de-DE. | |
| steppers | "split" | "end" | "none" | "split" | Where the minus and plus controls sit. |
| unit / prefix | ReactNode | Shown after or before the number, outside the value. | |
| scrubLabel | ReactNode | A label that changes the value when dragged horizontally. | |
| onValueCommitted | (value) => void | Fires on blur, Enter or a finished stepper press. Save here. | |
| size | "sm" | "default" | "lg" | "default" | Height. Matches Input and Button sizes. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add number-input# or, with the registry in components.jsonnpx shadcn@latest add @prodular/number-input