Skip to content

Number inputs

<div class="number-input">
<button
type="button"
class="number-input-step"
aria-label="Decrease"
onclick="this.parentElement.querySelector('input').stepDown()"
>
<i class="ti ti-minus" aria-hidden="true"></i>
</button>
<input
class="number-input-field"
type="number"
value="3"
min="0"
max="10"
aria-label="Quantity"
/>
<button
type="button"
class="number-input-step"
aria-label="Increase"
onclick="this.parentElement.querySelector('input').stepUp()"
>
<i class="ti ti-plus" aria-hidden="true"></i>
</button>
</div>
<div class="number-input">
<button
type="button"
class="number-input-step"
aria-label="Decrease"
onclick="this.parentElement.querySelector('input').stepDown()"
>
<i class="ti ti-minus" aria-hidden="true"></i>
</button>
<input
class="number-input-field"
type="number"
value="50"
min="0"
max="100"
step="5"
aria-label="Threshold"
/>
<button
type="button"
class="number-input-step"
aria-label="Increase"
onclick="this.parentElement.querySelector('input').stepUp()"
>
<i class="ti ti-plus" aria-hidden="true"></i>
</button>
</div>
<div class="number-input number-input-sm">
<button type="button" class="number-input-step" aria-label="Decrease">
<i class="ti ti-minus" aria-hidden="true"></i>
</button>
<input
class="number-input-field"
type="number"
value="1"
aria-label="Small"
/>
<button type="button" class="number-input-step" aria-label="Increase">
<i class="ti ti-plus" aria-hidden="true"></i>
</button>
</div>
<div class="number-input">
<button type="button" class="number-input-step" aria-label="Decrease">
<i class="ti ti-minus" aria-hidden="true"></i>
</button>
<input
class="number-input-field"
type="number"
value="1"
aria-label="Medium"
/>
<button type="button" class="number-input-step" aria-label="Increase">
<i class="ti ti-plus" aria-hidden="true"></i>
</button>
</div>
<div class="number-input number-input-lg">
<button type="button" class="number-input-step" aria-label="Decrease">
<i class="ti ti-minus" aria-hidden="true"></i>
</button>
<input
class="number-input-field"
type="number"
value="1"
aria-label="Large"
/>
<button type="button" class="number-input-step" aria-label="Increase">
<i class="ti ti-plus" aria-hidden="true"></i>
</button>
</div>
<NumberInput
defaultValue={1499}
min={0}
step={1}
format={{ style: "currency", currency: "USD" }}
inputAriaLabel="Price"
/>
Prop Type Default
size "sm" | "md" | "lg" "md"
placeholder string
inputAriaLabel string
decrementLabel string "Decrease"
incrementLabel string "Increase"
decrementIcon ReactNode glyph
incrementIcon ReactNode + glyph
classNames slots

Renders the whole group — steppers, field, ARIA — from one component, so there are no sub-parts to compose; classNames covers group, decrement, input, increment. inputAriaLabel names the field when there’s no associated <label>; inside a Field the label supplies the name instead.

Wraps Base UI NumberField, which owns value / defaultValue / onValueChange, min, max, step, format, clamp-on-blur, and scrub-to-change. format takes Intl.NumberFormat options.

format is React-only: a formatted 1,000 is not a valid type="number" value, so displaying it means splitting display from value, which needs the wrapper. The vanilla bundle gets native stepping and constraint validation, but not grouped thousands.

Class Effect
number-input Connected − / field / + group: bordered 0.5rem-radius shell, focus ring on :focus-within
number-input-field Borderless field inside it: right-aligned tabular digits, native spinners hidden
number-input-step 2rem-wide stepper button, divided from the field, hover tint
number-input-sm text-xs field, 1.75rem steppers
number-input-lg text-base field, 2.25rem steppers
number-input-root display: contents — the React wrapper element, invisible to layout

There is no number-input-md — it’s the unmodified number-input. Step buttons in vanilla call the platform stepUp() / stepDown(), which also honour min, max and step; each needs its own aria-label, and the field needs one too unless a <label> is associated. Native spinners are hidden in both engines, so the visible steppers are the only affordance. Digits are right-aligned and tabular so a column of values lines up.