Skip to content
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

Props

  • min
  • max
  • step
  • largeStep
  • clampOnBlur
  • size
  • disabled
  • readOnly
  • invalid
  • required
  • name
  • showStepButtons
  • decrementLabel
  • incrementLabel
  • locale
  • label
  • defaultValue
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.

  • sm

  • mddefault

  • lg

  • xl

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

const [guests, setGuests] = React.useState(2)
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

NameTypeValuesDefaultDescription
valuenumber | null--The value when you control it. null is an empty field.
defaultValuenumber | null-nullThe 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.
minnumber--Lowest value. Buttons and ArrowDown stop here.
maxnumber--Highest value. Must be greater than min.
stepnumber-1Amount per step.
largeStepnumber--Amount for PageUp and PageDown. Defaults to step times 10.
clampOnBlurboolean-trueClamp and round a typed value when the field loses focus. When false, an out-of-range value stays and the field shows invalid.
sizeenumsm | md | lg | xlmdControl height from size-element-sm/md/lg/xl. Pick per region; md is the default.
disabledboolean-falseNo typing, no buttons, disabled look.
readOnlyboolean-falseThe 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.
invalidboolean-falseSets aria-invalid and the danger border.
requiredboolean-falseNative required on the input.
namestring--Form field name; the submitted value is the plain number, not the formatted text.
showStepButtonsboolean-trueShow the decrement and increment buttons. Hide them for dense tables where arrow keys are enough.
decrementLabelstring-DecreaseAccessible name of the decrement button. Vietnamese: Giảm.
incrementLabelstring-IncreaseAccessible name of the increment button. Vietnamese: Tăng.
localestring--BCP 47 locale for formatting and for the separators used in parsing. Defaults to the document language.
formatOptionsIntl.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

NameRequiredDescription
leadingTextnoOptional fixed text before the number, such as a currency (follows the planned Input leadingText).
trailingTextnoOptional unit after the number, such as kg.

Anatomy

PartDescription
rootThe group that draws the field box: border, fill, corners, focus ring through :has(). The step buttons sit inside it at the end.
inputThe 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.
decrementThe lower ghost icon button of the stack at the end (Tabler chevron down, 16px); first of the pair on touch.
incrementThe upper ghost icon button of the stack at the end (Tabler chevron up, 16px); second of the pair on touch.
placeholderThe placeholder text of the input, in secondary text like Input.

Accessibility

Role: spinbutton

KeysAction
ArrowUp / ArrowDownIncrease or decrease by step, clamped to min and max.
PageUp / PageDownIncrease or decrease by largeStep.
Home / EndSet min or max when they are set.
EnterCommit the typed text: parse, clamp, round to step.
TabMoves 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-input

Run 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.