Skip to content

ComponentsForms

Number Field

A numeric input with increment and decrement steppers. Formats and parses numbers in the user's locale, supports currency, percent and unit formatting, min/max/step constraints, keyboard and scroll-wheel stepping, validation and form submission.

React AriaSource

Installation

pnpm dlx shadcn@latest add @desyne/number-field

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import { NumberField } from "@/components/ui/number-field";
tsx
<NumberField label="Quantity" name="quantity" defaultValue={1} minValue={0} />

The value is always a number. The field formats it for display with formatOptions and the user's locale, parses what the user types, and on blur clamps it to minValue / maxValue and snaps it to step.

An empty field is NaN

When the user clears the input, onChange receives NaN, not 0 or null. Check with Number.isNaN(value) before doing arithmetic, and use isRequired if an empty value isn't allowed.

When to use

  • Number Field — quantities, prices, percentages, measurements: any value that is really a number and benefits from steppers, formatting or a range.
  • Slider — picking an approximate value in a range, where the exact number matters less than its position.
  • Text Field — number-like strings that aren't quantities: phone numbers, postal codes, card numbers, IDs. Use inputMode="numeric" there.
  • Input OTP — one-time verification codes.

Anatomy

tsx
<NumberField>              {/* root: <div>, owns the number value and validation */}
  <Label />                {/* from `label` */}
  <FieldGroup>             {/* role="group": the visible box */}
    <Button slot="decrement" />  {/* stepper="split" only */}
    <FieldInput />               {/* the text <input> */}
    <Button slot="increment" />  {/* stepper="split" only */}
    <div>                        {/* stepper="stacked" only */}
      <Button slot="increment" />
      <Button slot="decrement" />
    </div>
  </FieldGroup>
  <Description />          {/* from `description` */}
  <FieldError />           {/* shown only while invalid */}
</NumberField>
PartRendersNotes
NumberField<div>Root. Carries data-slot="number-field", the group/field class and all state attributes.
Label<label>Rendered when label is set.
FieldGroup<div role="group">The visible box. Groups the input and steppers for assistive technology.
FieldInput<input type="text">Shows the formatted value. inputMode is chosen from minValue and the format so the right mobile keyboard appears.
Increment / decrement<button>Chevrons (stacked) or plus/minus (split). Labelled "Increase …" / "Decrease …", excluded from the tab order, disabled at the bounds. Hold to repeat.
Description<span slot="description">Rendered when description is set.
FieldError<span slot="errorMessage">Rendered only while invalid.
Hidden <input><input type="hidden">Rendered when name is set. Submits the raw number, not the formatted text.

Examples

Variants

outline (default), filled for dense or tinted surfaces, and underlined for minimal forms. The same variants apply to every field component.

Sizes

sm (28px), md (32px, default) and lg (40px), matching the buttons and other fields.

Stepper layouts

stepper="stacked" (default) puts compact chevrons on the right. split puts larger − and + buttons on either side, which suits touch and quantity pickers. none hides the buttons; the arrow keys and scroll wheel still work.

Default. Compact, right-aligned.
Bigger targets for touch.
Keyboard and wheel only.

Formatting

formatOptions takes any Intl.NumberFormat option: currency, percent, units, fraction digits, sign display, grouping. Users can type in their locale's format, including the currency symbol or percent sign. Percent values are fractions: 0.15 displays as 15%.

Range and step

minValue and maxValue bound the value; the steppers disable at each end. step sets the increment, and typed values snap to the nearest step (counted from minValue) on blur. Fractional steps work with any format.

Between 1 and 20 seats.
Snaps to 15-minute steps.

Disabled and read only

isDisabled disables the input and steppers. isReadOnly keeps the formatted value focusable and selectable but blocks edits and stepping.

Contact sales to change seats on Enterprise.

Controlled

value and onChange let you derive UI from the number, like a live total. onChange fires when the value is committed (on blur, Enter or a step), not on every keystroke.

Total: $60.00 / month

Required and range validation

By default an out-of-range value is clamped on blur. Set commitBehavior="validate" to keep what the user typed and show a range error instead, so they notice the limit. With native validation, the form won't submit until the field is valid.

Up to 8 guests per booking.

Custom validation

validate receives the parsed number (NaN when empty) and returns an error string or null. With validationBehavior="aria" on the Form, errors show live.

A multiple of 8 between 8 and 512.

Form submission

Set name and the raw number is submitted through a hidden input: 1500, not €1,500.00, and 0.1, not 10%. An empty field submits an empty string.

Recipes

Cart quantities

Small split steppers in a line-item list, each with an aria-label that names the product, driving a live subtotal.

  • Heavyweight tee

    Sand / M · $38.00

  • Six-panel cap

    Olive · $29.00

  • Canvas tote

    Natural · $24.00

Subtotal$129.00

Resource limits

A settings list where each field is labelled by its row title via aria-labelledby, with unit formatting and per-row ranges.

Memory

RAM available to each instance.

Max instances

Upper bound for autoscaling.

Request timeout

Requests running longer are cancelled.

Accessibility

  • The input is a text field with aria-roledescription="number field", so screen readers announce it as a number field along with its formatted value.
  • The input and steppers are wrapped in a role="group" labelled by the field label.
  • Stepper buttons are named "Increase Seats" / "Decrease Seats" from the label (override with incrementAriaLabel / decrementAriaLabel). They're skipped in the tab order because the arrow keys do the same job.
  • Values are formatted and parsed with the user's locale, including their decimal and grouping separators and numbering system.
  • inputMode is set automatically (numeric, decimal or text) so mobile keyboards include a minus sign or decimal point only when needed.
  • The scroll wheel steps the value only while the field has focus. Disable it with isWheelDisabled in scrollable layouts.

Keyboard

KeyAction
↑ / ↓Increments / decrements by step
Page Up / Page DownIncrements / decrements by step
Home / EndSets the value to minValue / maxValue, when set
EnterCommits the typed value (parse, clamp, snap and format)

Styling

Data attributes

On the NumberField root. Children can react with group-data-*/field: variants.

AttributePresent when
data-disabledisDisabled is true
data-readonlyisReadOnly is true
data-requiredisRequired is true
data-invalidValidation failed, or isInvalid is true

On the FieldGroup: data-hovered, data-focus-within, data-focus-visible, data-invalid and data-disabled. On each stepper button: data-hovered, data-pressed, data-disabled (at a bound) and data-focus-visible.

Slots

data-slotElement
number-fieldRoot
label, description, field-errorField text
field-groupVisible box
field-input<input>

The input uses tabular-nums so digits don't shift while stepping. The box styles come from fieldVariants in field.tsx; see Text Field.

Render props

className also accepts a function of the field state, which includes the live state (numberValue, canIncrement, canDecrement):

tsx
<NumberField
  label="Balance"
  className={({ state }) => (state.numberValue < 0 ? "text-destructive" : "")}
/>

API Reference

NumberField

Prop

Type

Also accepts every prop of React Aria's NumberField.

  • Slider — for approximate values in a range.
  • Text Field — for number-like strings, and the shared field building blocks.
  • Select — shares the same field variants and sizes.