useNumberField

Provides the behavior and accessibility implementation for a number field component. Number fields allow users to enter a number, and increment or decrement the value using stepper buttons.

installyarn add react-aria
version3.19.0
usageimport {useNumberField} from 'react-aria'

API#


useNumberField( props: AriaNumberFieldProps, state: NumberFieldState, inputRef: RefObject<HTMLInputElement> ): NumberFieldAria

Features#


Number fields can be built with <input type="number">, but the behavior is very inconsistent across browsers and platforms, it supports limited localized formatting options, and it is challenging to style the stepper buttons. useNumberField helps achieve accessible number fields that support internationalized formatting options and can be styled as needed.

  • Support for internationalized number formatting and parsing including decimals, percentages, currency values, and units
  • Support for the Latin, Arabic, and Han positional decimal numbering systems in over 30 locales
  • Automatically detects the numbering system used and supports parsing numbers not in the default numbering system for the locale
  • Support for multiple currency formats including symbol, code, and name in standard or accounting notation
  • Validates keyboard entry as the user types so that only valid numeric input according to the locale and numbering system is accepted
  • Handles composed input from input method editors, e.g. Pinyin
  • Automatically selects an appropriate software keyboard for mobile according to the current platform and allowed values
  • Supports rounding to a configurable number of fraction digits
  • Support for clamping the value between a configurable minimum and maximum, and snapping to a step value
  • Support for stepper buttons and arrow keys to increment and decrement the value according to the step value
  • Supports pressing and holding the stepper buttons to continuously increment or decrement
  • Handles floating point rounding errors when incrementing, decrementing, and snapping to step
  • Supports using the scroll wheel to increment and decrement the value
  • Exposed to assistive technology as a text field with a custom localized role description using ARIA
  • Follows the spinbutton ARIA pattern
  • Works around bugs in VoiceOver with the spinbutton role
  • Uses an ARIA live region to ensure that value changes are announced
  • Support for description and error message help text linked to the input via ARIA

Read our blog post for more details about the interactions and internationalization support implemented by useNumberField.

Anatomy#


1280LabelInputLabelIncrement buttonDecrement buttonGroup

Number fields consist of an input element that shows the current value and allows the user to type a new value, optional stepper buttons to increment and decrement the value, a group containing the input and stepper buttons, and a label.

useNumberField also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the aria-describedby attribute.

useNumberField returns props for each of these, which you should spread onto the appropriate elements:

NameTypeDescription
labelPropsLabelHTMLAttributes<HTMLLabelElement>Props for the label element.
groupPropsDOMAttributesProps for the group wrapper around the input and stepper buttons.
inputPropsInputHTMLAttributes<HTMLInputElement>Props for the input element.
incrementButtonPropsAriaButtonPropsProps for the increment button, to be passed to useButton.
decrementButtonPropsAriaButtonPropsProps for the decrement button, to be passed to useButton.
descriptionPropsDOMAttributesProps for the number field's description element, if any.
errorMessagePropsDOMAttributesProps for the number field's error message element, if any.

State is managed by the useNumberFieldState hook from @react-stately/numberfield. The state object should be passed as an option to useNumberField

If there is no visual label, an aria-label or aria-labelledby prop must be passed instead to identify the element to screen readers.

Example#


The following example shows how to build a simple number field. It includes an input element where the user can type a number, along with increment and decrement buttons on either side. The Button component used in this example is independent and can be used separately from NumberField.

Note: Due to a bug in Safari on macOS, pointer events may not be dispatched after a <button> element is disabled while the mouse is pressed. This may require the user to click twice when incrementing or decrementing the value from the minimum or maximum value. While out of scope for this example, you may wish to use a <div> element instead of a <button> to avoid this issue. See the useButton docs for an example of a button with a custom element type.

In addition, see useTextField for an example of the description and error message elements.

import {useNumberFieldState} from 'react-stately';
import {useLocale, useNumberField} from 'react-aria';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function NumberField(props) {
  let { locale } = useLocale();
  let state = useNumberFieldState({ ...props, locale });
  let inputRef = React.useRef();
  let {
    labelProps,
    groupProps,
    inputProps,
    incrementButtonProps,
    decrementButtonProps
  } = useNumberField(props, state, inputRef);

  return (
    <div>
      <label {...labelProps}>{props.label}</label>
      <div {...groupProps}>
        <Button {...decrementButtonProps}>-</Button>
        <input {...inputProps} ref={inputRef} />
        <Button {...incrementButtonProps}>+</Button>
      </div>
    </div>
  );
}

<NumberField
  label="Price"
  defaultValue={6}
  formatOptions={{
    style: 'currency',
    currency: 'USD'
  }}
/>
import {useNumberFieldState} from 'react-stately';
import {useLocale, useNumberField} from 'react-aria';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function NumberField(props) {
  let { locale } = useLocale();
  let state = useNumberFieldState({ ...props, locale });
  let inputRef = React.useRef();
  let {
    labelProps,
    groupProps,
    inputProps,
    incrementButtonProps,
    decrementButtonProps
  } = useNumberField(props, state, inputRef);

  return (
    <div>
      <label {...labelProps}>{props.label}</label>
      <div {...groupProps}>
        <Button {...decrementButtonProps}>-</Button>
        <input {...inputProps} ref={inputRef} />
        <Button {...incrementButtonProps}>+</Button>
      </div>
    </div>
  );
}

<NumberField
  label="Price"
  defaultValue={6}
  formatOptions={{
    style: 'currency',
    currency: 'USD'
  }}
/>
import {useNumberFieldState} from 'react-stately';
import {
  useLocale,
  useNumberField
} from 'react-aria';

// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';

function NumberField(
  props
) {
  let { locale } =
    useLocale();
  let state =
    useNumberFieldState({
      ...props,
      locale
    });
  let inputRef = React
    .useRef();
  let {
    labelProps,
    groupProps,
    inputProps,
    incrementButtonProps,
    decrementButtonProps
  } = useNumberField(
    props,
    state,
    inputRef
  );

  return (
    <div>
      <label
        {...labelProps}
      >
        {props.label}
      </label>
      <div
        {...groupProps}
      >
        <Button
          {...decrementButtonProps}
        >
          -
        </Button>
        <input
          {...inputProps}
          ref={inputRef}
        />
        <Button
          {...incrementButtonProps}
        >
          +
        </Button>
      </div>
    </div>
  );
}

<NumberField
  label="Price"
  defaultValue={6}
  formatOptions={{
    style: 'currency',
    currency: 'USD'
  }}
/>

Button#

The Button component is used in the above example to increment and decrement the value. It is built using the useButton hook, and can be shared with many other components.

Show code
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React.useRef();
  let { buttonProps } = useButton(props, ref);
  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React.useRef();
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React
    .useRef();
  let { buttonProps } =
    useButton(
      props,
      ref
    );
  return (
    <button
      {...buttonProps}
      ref={ref}
    >
      {props.children}
    </button>
  );
}

Usage#


The following examples show how to use the NumberField component created in the above example.

Controlled#

By default, NumberField is uncontrolled. However, when using the value prop, it becomes controlled. This allows you to store the current value in your own state, and use it elsewhere.

The onChange event is triggered whenever the number value updates. This happens when the user types a value and blurs the input, or when incrementing or decrementing the value. It does not happen as the user types because partial input may not be parseable to a valid number.

function Example() {
  let [value, setValue] = React.useState(6);

  return (
    <>
      <NumberField
        label="Controlled value"
        value={value}
        onChange={setValue} />
      <div>Current value prop: {value}</div>
    </>
  );
}
function Example() {
  let [value, setValue] = React.useState(6);

  return (
    <>
      <NumberField
        label="Controlled value"
        value={value}
        onChange={setValue} />
      <div>Current value prop: {value}</div>
    </>
  );
}
function Example() {
  let [value, setValue] =
    React.useState(6);

  return (
    <>
      <NumberField
        label="Controlled value"
        value={value}
        onChange={setValue}
      />
      <div>
        Current value
        prop: {value}
      </div>
    </>
  );
}

Decimals#

The default formatting style for NumberField is decimal, but you can configure various aspects via the formatOptions prop. All options supported by Intl.NumberFormat are supported, including configuration of minimum and maximum fraction digits, sign display, grouping separators, etc. Currently only standard notation is supported, though scientific, engineering, and compact notation may be supported in the future.

The following example uses the signDisplay option to include the plus sign for positive numbers, for example to display an offset from some value. In addition, it always displays a minimum of 1 digit after the decimal point, and allows up to 2 fraction digits. If the user enters more than 2 fraction digits, the result will be rounded.

<NumberField
  label="Adjust exposure"
  defaultValue={0}
  formatOptions={{
    signDisplay: 'exceptZero',
    minimumFractionDigits: 1,
    maximumFractionDigits: 2
  }} />
<NumberField
  label="Adjust exposure"
  defaultValue={0}
  formatOptions={{
    signDisplay: 'exceptZero',
    minimumFractionDigits: 1,
    maximumFractionDigits: 2
  }} />
<NumberField
  label="Adjust exposure"
  defaultValue={0}
  formatOptions={{
    signDisplay:
      'exceptZero',
    minimumFractionDigits:
      1,
    maximumFractionDigits:
      2
  }}
/>

Percentages#

The style: 'percent' option can be passed to the formatOptions prop to treat the value as a percentage. In this mode, the value is multiplied by 100 before it is displayed, i.e. 0.45 is displayed as 45%. The reverse is also true: when the user enters a value, the onChange event will be triggered with the entered value divided by 100. When the percent option is enabled, the default step automatically changes to 0.01 such that incrementing and decrementing occurs by 1%. This can be overridden with the step prop. See below for details.

<NumberField
  label="Sales tax"
  defaultValue={0.05}
  formatOptions={{
    style: 'percent'
  }} />
<NumberField
  label="Sales tax"
  defaultValue={0.05}
  formatOptions={{
    style: 'percent'
  }} />
<NumberField
  label="Sales tax"
  defaultValue={0.05}
  formatOptions={{
    style: 'percent'
  }} />

Currency values#

The style: 'currency' option can be passed to the formatOptions prop to treat the value as a currency value. The currency option must also be passed to set the currency code (e.g. USD) to use. In addition, the currencyDisplay option can be used to choose whether to display the currency symbol, currency code, or currency name. Finally, the currencySign option can be set to accounting to use accounting notation for negative numbers, which uses parentheses rather than a minus sign in some locales.

If you need to allow the user to change the currency, you should include a separate dropdown next to the number field. The number field itself will not determine the currency from the user input.

<NumberField
  label="Transaction amount"
  defaultValue={45}
  formatOptions={{
    style: 'currency',
    currency: 'EUR',
    currencyDisplay: 'code',
    currencySign: 'accounting'
  }} />
<NumberField
  label="Transaction amount"
  defaultValue={45}
  formatOptions={{
    style: 'currency',
    currency: 'EUR',
    currencyDisplay: 'code',
    currencySign: 'accounting'
  }} />
<NumberField
  label="Transaction amount"
  defaultValue={45}
  formatOptions={{
    style: 'currency',
    currency: 'EUR',
    currencyDisplay:
      'code',
    currencySign:
      'accounting'
  }}
/>

Units#

The style: 'unit' option can be passed to the formatOptions prop to format the value with a unit of measurement. The unit option must also be passed to set which unit to use (e.g. inch). In addition, the unitDisplay option can be used to choose whether to display the unit in long, short, or narrow format.

If you need to allow the user to change the unit, you should include a separate dropdown next to the number field. The number field itself will not determine the unit from the user input.

Note: the unit style is not currently supported in Safari. A polyfill may be necessary.

<NumberField
  label="Package width"
  defaultValue={4}
  formatOptions={{
    style: 'unit',
    unit: 'inch',
    unitDisplay: 'long'
  }} />
<NumberField
  label="Package width"
  defaultValue={4}
  formatOptions={{
    style: 'unit',
    unit: 'inch',
    unitDisplay: 'long'
  }} />
<NumberField
  label="Package width"
  defaultValue={4}
  formatOptions={{
    style: 'unit',
    unit: 'inch',
    unitDisplay: 'long'
  }} />

Minimum and maximum values#

The minValue and maxValue props can be used to limit the entered value to a specific range. The value will be clamped when the user blurs the input field. In addition, the increment and decrement buttons will be disabled when the value is within one step value from the bounds (see below for info about steps). Ranges can be open ended by only providing either minValue or maxValue rather than both.

If a valid range is known ahead of time, it is a good idea to provide it to NumberField so it can optimize the experience. For example, when the minimum value is greater than or equal to zero, it is possible to use a numeric keyboard on iOS rather than a full text keyboard (necessary to enter a minus sign).

<NumberField
  label="Enter your age"
  minValue={0} />
<NumberField
  label="Enter your age"
  minValue={0} />
<NumberField
  label="Enter your age"
  minValue={0} />

Step values#

The step prop can be used to snap the value to certain increments. If there is a minValue defined, the steps are calculated starting from the minimum. For example, if minValue={2}, and step={3}, the valid step values would be 2, 5, 8, 11, etc. If no minValue is defined, the steps are calculated starting from zero and extending in both directions. In other words, such that the values are evenly divisible by the step. If no step is defined, any decimal value may be typed, but incrementing and decrementing snaps the value to an integer.

If the user types a value that is between two steps and blurs the input, the value will be snapped to the nearest step. When incrementing or decrementing, the value is snapped to the nearest step that is higher or lower, respectively. When incrementing or decrementing from an empty field, the value starts at the minValue or maxValue, respectively, if defined. Otherwise, the value starts from 0.

<NumberField
  label="Step"
  step={10} />
<NumberField
  label="Step + minValue"
  minValue={2}
  step={3} />
<NumberField
  label="Step + minValue + maxValue"
  minValue={2}
  maxValue={21}
  step={3} />
<NumberField
  label="Step"
  step={10} />
<NumberField
  label="Step + minValue"
  minValue={2}
  step={3} />
<NumberField
  label="Step + minValue + maxValue"
  minValue={2}
  maxValue={21}
  step={3} />
<NumberField
  label="Step"
  step={10}
/>
<NumberField
  label="Step + minValue"
  minValue={2}
  step={3}
/>
<NumberField
  label="Step + minValue + maxValue"
  minValue={2}
  maxValue={21}
  step={3}
/>

Disabled and read only#

The isDisabled and isReadOnly props can be used prevent the user from editing the value of the number field. The difference is that isReadOnly still allows the input to be focused, while isDisabled prevents all user interaction.

<NumberField label="Disabled" isDisabled value={25} />
<NumberField label="Read only" isReadOnly value={32} />
<NumberField label="Disabled" isDisabled value={25} />
<NumberField label="Read only" isReadOnly value={32} />
<NumberField
  label="Disabled"
  isDisabled
  value={25}
/>
<NumberField
  label="Read only"
  isReadOnly
  value={32}
/>

Internationalization#


useNumberField handles many aspects of internationalization automatically, including formatting and parsing numbers according to the current locale and numbering system. In addition, the increment and decrement buttons have localized ARIA labels. You are responsible for localizing the label text passed into the number field.

RTL#

In right-to-left languages, the number field should be mirrored. The order of the input and buttons should be flipped, and the input text should be right aligned instead of left aligned. Ensure that your CSS accounts for this.