Components
NumberInput
A numeric field with stacked step buttons at its end, for quantities, counts and amounts with a known step (cart quantity, guest count, seats). It is a text input (type text, inputmode decimal), never type number, so it can show formatted text such as 1.000 ₫. Use Input for free text, Slider when the exact value matters less than the range.
Preview
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { NumberInput } from "@/components/ui/number-input"
<Field className="w-full max-w-sm" invalid={false}>
<FieldLabel>Quantity</FieldLabel>
<FieldControl>
<NumberInput min={0} max={10} step={1} defaultValue={3} />
</FieldControl>
</Field>Usage
When to use
- Enter or adjust a quantity whose valid steps are known, such as seats, guests or items in a cart.
- Show a number with locale-aware formatting, such as a currency amount, while keeping it editable.
When not to use
- Enter an identifier such as a phone or card number. Use Input with numeric input mode.
- Pick a position where the range matters more than the exact number. Use Slider.
- Enter free-form text. Use Input.
Variants
size
Control height from size-element-sm/md/lg/xl. Pick per region; md is the default.
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { NumberInput } from "@/components/ui/number-input"
<Field className="w-full max-w-sm" invalid={false}>
<FieldLabel>Quantity</FieldLabel>
<FieldControl>
<NumberInput min={0} max={10} size="sm" step={1} defaultValue={3} />
</FieldControl>
</Field>
<Field className="w-full max-w-sm" invalid={false}>
<FieldLabel>Quantity</FieldLabel>
<FieldControl>
<NumberInput min={0} max={10} step={1} defaultValue={3} />
</FieldControl>
</Field>
<Field className="w-full max-w-sm" invalid={false}>
<FieldLabel>Quantity</FieldLabel>
<FieldControl>
<NumberInput min={0} max={10} size="lg" step={1} defaultValue={3} />
</FieldControl>
</Field>
<Field className="w-full max-w-sm" invalid={false}>
<FieldLabel>Quantity</FieldLabel>
<FieldControl>
<NumberInput min={0} max={10} size="xl" step={1} defaultValue={3} />
</FieldControl>
</Field>Examples
Quantity
import { Field } from "@/components/ui/field"
import { NumberInput } from "@/components/ui/number-input"
<Field label="Quantity">
<NumberInput defaultValue={1} min={1} max={10} />
</Field>Controlled
import { NumberInput } from "@/components/ui/number-input"
const [guests, setGuests] = React.useState(2)
<NumberInput value={guests} onValueChange={setGuests} min={1} max={16} size="lg" />Currency
import { NumberInput } from "@/components/ui/number-input"
<NumberInput defaultValue={1000} step={1000} min={0} locale="vi-VN" formatOptions={{ style: "currency", currency: "VND" }} />Do and don't
Do
- Set min and max whenever the domain has them (a quantity is at least 1).
- Use inside Field so the label and error are linked.
- Use lg or xl on touch-first screens so the buttons are easy to hit.
Don't
- Do not use for numbers that are identifiers (card number, phone, postal code); use Input with inputmode numeric.
- Do not use for a range where position matters more than the value; use Slider.
- Do not fire network requests on every step; debounce in the app.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| value | number | null | - | - | The value when you control it. null is an empty field. |
| defaultValue | number | null | - | null | The value on first render when you do not control it. |
| onValueChange | (value: number | null) => void | - | - | Called with the committed number (after parse, clamp and step rounding) on a step key, a button press, Enter or blur, and with null when the field is cleared. Not called for text that does not parse (the field shows invalid instead) or on every keystroke. |
| min | number | - | - | Lowest value. Buttons and ArrowDown stop here. |
| max | number | - | - | Highest value. Must be greater than min. |
| step | number | - | 1 | Amount per step. |
| largeStep | number | - | - | Amount for PageUp and PageDown. Defaults to step times 10. |
| clampOnBlur | boolean | - | true | Clamp and round a typed value when the field loses focus. When false, an out-of-range value stays and the field shows invalid. |
| size | enum | sm | md | lg | xl | md | Control height from size-element-sm/md/lg/xl. Pick per region; md is the default. |
| disabled | boolean | - | false | No typing, no buttons, disabled look. |
| readOnly | boolean | - | false | The value can be read, focused and copied but not changed; the step buttons are hidden and the field drops its fill. The inner input gets the native readOnly attribute (its bindings use :read-only); the root group sets data-read-only. |
| invalid | boolean | - | false | Sets aria-invalid and the danger border. |
| required | boolean | - | false | Native required on the input. |
| name | string | - | - | Form field name; the submitted value is the plain number, not the formatted text. |
| showStepButtons | boolean | - | true | Show the decrement and increment buttons. Hide them for dense tables where arrow keys are enough. |
| decrementLabel | string | - | Decrease | Accessible name of the decrement button. Vietnamese: Giảm. |
| incrementLabel | string | - | Increase | Accessible name of the increment button. Vietnamese: Tăng. |
| locale | string | - | - | BCP 47 locale for formatting and for the separators used in parsing. Defaults to the document language. |
| formatOptions | Intl.NumberFormatOptions | - | - | How the value is shown when the field is not being edited and in aria-valuetext: currency, percent, unit, fraction digits. While focused the field shows the plain localized number. |
Slots
| Name | Required | Description |
|---|---|---|
| leadingText | no | Optional fixed text before the number, such as a currency (follows the planned Input leadingText). |
| trailingText | no | Optional unit after the number, such as kg. |
Anatomy
| Part | Description |
|---|---|
| root | The group that draws the field box: border, fill, corners, focus ring through :has(). The step buttons sit inside it at the end. |
| input | The text input with role spinbutton, font-size-input, tabular numbers, aligned to the start (left in LTR, right in RTL), with the step buttons at the end. |
| decrement | The lower ghost icon button of the stack at the end (Tabler chevron down, 16px); first of the pair on touch. |
| increment | The upper ghost icon button of the stack at the end (Tabler chevron up, 16px); second of the pair on touch. |
| placeholder | The placeholder text of the input, in secondary text like Input. |
Accessibility
Role: spinbutton
| Keys | Action |
|---|---|
| ArrowUp / ArrowDown | Increase or decrease by step, clamped to min and max. |
| PageUp / PageDown | Increase or decrease by largeStep. |
| Home / End | Set min or max when they are set. |
| Enter | Commit the typed text: parse, clamp, round to step. |
| Tab | Moves to the next field. The step buttons are skipped (tabIndex -1); the keys above cover them. |
- The input has type text, inputmode decimal (numeric when step is an integer and min is at least 0) and role spinbutton with aria-valuenow, aria-valuemin and aria-valuemax.
- aria-valuetext is the value formatted with formatOptions and locale, for example 1.000 ₫, so screen readers say the same text the field shows.
- Parsing: the group and decimal separators of the locale come from Intl.NumberFormat formatToParts. Group separators are stripped, the decimal separator maps to a dot, then the text is parsed. A string that does not parse sets the invalid state and does not call onValueChange.
- Clearing the field calls onValueChange(null).
- Step buttons are real buttons named by decrementLabel and incrementLabel, disabled at min and max, skipped by Tab (tabIndex -1) because the arrow keys cover them.
- Layout: on a fine pointer the two buttons stack at the end of the field, increment on top. On touch (pointer: coarse) they sit side by side at the end, decrement then increment, each at least size-target-min (44px), with a color-border divider on each button's start edge so the touch area is visible.
- Holding a step button repeats after a short delay; the announcement is the final value, not each step.
- invalid sets aria-invalid; Field passes its error with aria-describedby.
Install
npx werewolf-ui add number-inputRun npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.