Skip to main contentSkip to docs navigation

Number Field

A text input for numbers, with increment and decrement buttons, the arrow keys, a range and a step, and values formatted for the locale.

Overview

NumberField is a text input for a number, built on React Aria's useNumberField. The buttons at its end add or take away one step, and so do the Up and Down arrow keys and Page Up and Page Down; Home and End go to min and max. Typed text is parsed in the locale's own format, and the value is shown formatted once the field loses focus. Like Text Input, it takes label, help and the validation props, and renders a FormLabel, FormHelp and FormFeedback wired to the input.

JSX
import { NumberField } from '@chassis-ui/react'

export const Example = () => <NumberField defaultValue={1} label="Quantity" />

The input is a .form-input wrapper around a .ghost-input, chassis-css's input adorn shape, with the two buttons in a .number-field-buttons column at its end. chassis-css has no number field, so the column's styles are in this package's own stylesheet, @chassis-ui/react/style.css (see Theming). They read the field's padding, border and colors, so every size, state and theme of .form-input applies to the buttons too.

Range and step

min and max bound the value: a button is disabled at its end of the range, and a value typed past it is brought back inside when the field loses focus. step sets how much a button or an arrow key changes the value, and a typed value snaps to it, counted from min.

From 2 to 12, in steps of 2.
JSX
import { NumberField } from '@chassis-ui/react'

export const Example = () => (
  <NumberField
    defaultValue={4}
    help="From 2 to 12, in steps of 2."
    label="Seats"
    max={12}
    min={2}
    step={2}
  />
)

Formatting

formatOptions takes the options of Intl.NumberFormat: decimals, a percent, a currency or a unit, with the locale's separators and symbols. The value stays a plain number: 0.15 for 15%. The locale is the browser's, or the one an I18nProvider sets.

JSX
import { NumberField } from '@chassis-ui/react'

export const Example = () => (
  <>
    <NumberField
      defaultValue={1250.5}
      formatOptions={{ currency: 'EUR', style: 'currency' }}
      label="Price"
    />
    <NumberField
      defaultValue={0.15}
      formatOptions={{ style: 'percent' }}
      label="Discount"
      step={0.01}
    />
    <NumberField
      defaultValue={72}
      formatOptions={{ style: 'unit', unit: 'kilogram' }}
      label="Weight"
    />
  </>
)

Controlled value

Pass value and onChange to control the value. onChange receives the number when it is committed: on a step (a button, an arrow key, Home, End, the mouse wheel while the field has focus), on Enter or when the field loses focus after typing, on a paste that replaces the whole text, and on a form reset. An empty field is NaN. Omit them (optionally passing defaultValue instead) to let the component manage its own value.

Current value: 3
JSX
import { useState } from 'react'
import { NumberField } from '@chassis-ui/react'

export const Example = () => {
  const [value, setValue] = useState(3)
  return (
    <NumberField
      help={`Current value: ${isNaN(value) ? 'empty' : value}`}
      label="Guests"
      min={0}
      onChange={setValue}
      value={value}
    />
  )
}

In a form

With name, the field submits the number, not the text it shows: a hidden input carries the value. With form, the field belongs to a form elsewhere on the page, which submits and resets it.

JSX
import { useState } from 'react'
import { Button, NumberField, Stack } from '@chassis-ui/react'

export const Example = () => {
  const [submitted, setSubmitted] = useState('')
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(JSON.stringify(Object.fromEntries(new FormData(event.currentTarget))))
      }}
    >
      <Stack direction="vertical" gap="md" style={{ maxWidth: '20rem' }}>
        <NumberField
          defaultValue={1250.5}
          formatOptions={{ currency: 'EUR', style: 'currency' }}
          help={submitted ? `Submitted: ${submitted}` : undefined}
          label="Price"
          name="price"
        />
        <Button type="submit">Submit</Button>
      </Stack>
    </form>
  )
}

Sizing

JSX
import { NumberField } from '@chassis-ui/react'

export const Example = () => (
  <>
    <NumberField aria-label="Small number field" defaultValue={1} size="sm" />
    <NumberField aria-label="Default number field" defaultValue={1} />
    <NumberField aria-label="Large number field" defaultValue={1} size="lg" />
  </>
)

States

disabled disables the input and both buttons. readOnly keeps the value from changing, by typing or by a step. invalid and valid color the field and its buttons, and show invalidFeedback or validFeedback.

At most 100.
In stock.
JSX
import { NumberField } from '@chassis-ui/react'

export const Example = () => (
  <>
    <NumberField defaultValue={3} disabled label="Disabled" />
    <NumberField defaultValue={3} label="Read only" readOnly />
    <NumberField defaultValue={120} invalid invalidFeedback="At most 100." label="Invalid" />
    <NumberField defaultValue={42} label="Valid" valid validFeedback="In stock." />
  </>
)

Adorns and no buttons

adornStart and adornEnd render content at the input's edges, as on TextInput, with adornEnd before the buttons. stepButtons={false} leaves the buttons out; the arrow keys still step the value.

cm
JSX
import { InputAdorn, NumberField } from '@chassis-ui/react'

export const Example = () => (
  <>
    <NumberField adornEnd={<InputAdorn>cm</InputAdorn>} defaultValue={180} label="Height" />
    <NumberField defaultValue={2026} label="Year" stepButtons={false} />
  </>
)

The buttons draw the increment and decrement icons, which IconProvider's icons replaces for the whole page, and incrementIcon and decrementIcon for one field (see Icon).

Accessibility

The input keeps the text role, with the role description "number field", rather than a spin button, which some screen readers can't focus. The buttons are out of the tab order, since the arrow keys do what they do, and they are named from the field's label: "Increase Quantity", "Decrease Quantity", in the locale's language. incrementAriaLabel and decrementAriaLabel replace those names. A mouse click on a button leaves focus in the input; a tap focuses the button, so that no software keyboard opens.

On a phone, the keyboard is the one that has the keys the field needs: a minus sign when it takes negative numbers, a decimal separator when it takes decimals. The server can't tell which phone it renders for, so its HTML asks for the numeric keyboard, and the choice is made once the page has hydrated (see Server rendering).

API

NumberField

PropTypeDefault
adornEnd?ReactNode—
Content rendered at the input's trailing edge, before the step buttons, e.g. a unit as an InputAdorn.
adornStart?ReactNode—
Content rendered at the input's leading edge, e.g. an InputAdorn icon or text.
className?string—
A string of all className you want applied to the component.
decrementAriaLabel?string—
The name of the decrement button for screen readers. Defaults to "Decrease" and the field's label, in the locale's language.
decrementIcon?IconValue—
The decrement button's icon, in place of IconProvider's decrement.
defaultValue?number—
The value of the field, uncontrolled.
disabled?boolean—
Toggle the disabled state for the component.
formatOptions?NumberFormatOptions—
How the value is shown and typed: decimals, a percent, a currency or a unit, with the locale's separators. Intl.NumberFormat's options.
help?ReactNode—
A description for the field, rendered below the input.
incrementAriaLabel?string—
The name of the increment button for screen readers. Defaults to "Increase" and the field's label, in the locale's language.
incrementIcon?IconValue—
The increment button's icon, in place of IconProvider's increment.
invalid?boolean—
Set component validation state to invalid. Left unset, the component shows as invalid by itself from a submit it held back (an empty required one, say) until it no longer would, or its form is reset. Set it, to false too, and it alone decides.
invalidFeedback?ReactNode—
An error message for the field, rendered below the input when invalid is set, and after a submit the field held back, until it no longer would.
label?ReactNode—
The field's caption, rendered as a FormLabel associated with this input.
max?number—
The largest value. A larger one typed in is clamped to it when the field loses focus.
min?number—
The smallest value. A smaller one typed in is clamped to it when the field loses focus.
onChange?((value: number) => void)—
Handler that is called when the value is committed: on a step (a button, an arrow key, Home, End, the mouse wheel), on Enter or when the field loses focus after typing, on a paste that replaces the whole text, and on a form reset. NaN when the field is empty.
readOnly?boolean—
Toggle the readonly state for the component.
required?boolean—
Toggle the required state for the component.
size?"sm" | "lg"—
Size the component sm or lg.
step?number1
The amount the buttons and the arrow keys add or take away. A typed value snaps to it, counted from min.
stepButtons?booleantrue
Show the increment and decrement buttons. Without them the arrow keys still step the value.
valid?boolean—
Set component validation state to valid.
validFeedback?ReactNode—
A success message for the field, rendered below the input when valid is set.
value?number—
The value of the field, controlled. NaN for an empty field.