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.
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.
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.
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.
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.
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
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.
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.
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
| Prop | Type | Default |
|---|---|---|
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? | number | 1 |
The amount the buttons and the arrow keys add or take away. A typed value snaps to it,
counted from min. | ||
stepButtons? | boolean | true |
| 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. | ||