Skip to the content

Kongens Nytorv

Metro M1, M2 and M3.

From 1 to 20

Variants and states

RestDisabled
Default

From 1 to 20

From 1 to 20

Default
  • Rest

    From 1 to 20

  • Disabled

    From 1 to 20

NumberField styles hover, pressed and focus on its inner parts, which a still preview can't reach, so this table shows rest and disabled only.

Props

NameTypeDefaultDescription
label*ReactNode—Required. The field's name, in sentence case, such as "Departures to show": shown above the box, and the input's accessible name.
hiddenLabelbooleanfalseWhether the label is heard but not seen. Use it only where the screen around the field already says what it is for.
optionalbooleanfalseWhether the person may leave the field empty. Adds "(optional)" after the label's words, seen and heard as part of the input's name. It sets no check: required does.
optionalLabelstringThe locale'sOverrides "(optional)", the mark after the label's words with optional.
descriptionReactNode—A line of help between the label and the box, such as "From 1 to 20", read after the label by a screen reader. Left out, no description is rendered.
errorReactNode—What is wrong with the number and how to put it right, such as "Enter a number from 1 to 20". Given, it marks the field invalid and is shown under the box; left out, the field shows Base UI's own message when the number fails its checks.
disabledbooleanfalseWhether the field ignores the person: the box takes no focus, no typing and no presses, and the label, the help, the number and the buttons turn to the tertiary ink.
onChange(event: Event, value: number | null, details: NumberField.Root.ChangeEventDetails) => void—Called when the number changes: as the person types a number that parses (details.reason 'input-change'), clears the box ('input-clear', value null), leaves it and the number is clamped or rounded ('input-blur'), pastes ('input-paste'), steps it with the keys ('keyboard'), presses a button ('increment-press', 'decrement-press') or turns the wheel ('wheel'), or the browser autofills Base UI's hidden input ('none'). event is details.event, and details.cancel() keeps the number as it was.
onChangeCommitted(event: Event, value: number | null, details: NumberField.Root.CommitEventDetails) => void—Called once the person has settled on a number: when they leave the box after typing, release a held button, or with each key step and wheel turn; event is details.event. Use it to save or fetch, where onChange fires on every step of a held button.
localeLocalesArgumentThe PerronProvider's localeThe locale the number is written and read in: "1,500" in English, "1.500" in Danish, "1 500" in Swedish and Norwegian. Left out, the nearest PerronProvider's.
placeholderstring—Words shown in the empty box, such as "Such as 8". They vanish as the person types, so they never replace the label or the description.
increaseLabelstringThe locale'sOverrides "Increase", the increment button's name.
decreaseLabelstringThe locale'sOverrides "Decrease", the decrement button's name.
numberFieldLabelstringThe locale'sOverrides "Number field", what a screen reader calls the input in place of its role (Owned strings).
refRef<HTMLDivElement>—Reaches the root, the NumberField.Root <div>.
slotProps{ field?: Omit<FieldRootProps, "children" | "disabled" | "name"> | undefined; label?: FieldLabelProps | undefined; description?: FieldDescriptionProps | undefined; error?: FieldErrorProps | undefined; group?: NumberFieldGroupProps | undefined; input?: NumberFieldInputProps | undefined; decrement?: NumberFieldDecrementProps | undefined; increment?: NumberFieldIncrementProps | undefined; }—Props for each part: field, label, description, error, group, input, decrement, increment.
label (required)ReactNode, default —
Required. The field's name, in sentence case, such as "Departures to show": shown above the box, and the input's accessible name.
hiddenLabelboolean, default false
Whether the label is heard but not seen. Use it only where the screen around the field already says what it is for.
optionalboolean, default false
Whether the person may leave the field empty. Adds "(optional)" after the label's words, seen and heard as part of the input's name. It sets no check: required does.
optionalLabelstring, default The locale's
Overrides "(optional)", the mark after the label's words with optional.
descriptionReactNode, default —
A line of help between the label and the box, such as "From 1 to 20", read after the label by a screen reader. Left out, no description is rendered.
errorReactNode, default —
What is wrong with the number and how to put it right, such as "Enter a number from 1 to 20". Given, it marks the field invalid and is shown under the box; left out, the field shows Base UI's own message when the number fails its checks.
disabledboolean, default false
Whether the field ignores the person: the box takes no focus, no typing and no presses, and the label, the help, the number and the buttons turn to the tertiary ink.
onChange(event: Event, value: number | null, details: NumberField.Root.ChangeEventDetails) => void, default —
Called when the number changes: as the person types a number that parses (details.reason 'input-change'), clears the box ('input-clear', value null), leaves it and the number is clamped or rounded ('input-blur'), pastes ('input-paste'), steps it with the keys ('keyboard'), presses a button ('increment-press', 'decrement-press') or turns the wheel ('wheel'), or the browser autofills Base UI's hidden input ('none'). event is details.event, and details.cancel() keeps the number as it was.
onChangeCommitted(event: Event, value: number | null, details: NumberField.Root.CommitEventDetails) => void, default —
Called once the person has settled on a number: when they leave the box after typing, release a held button, or with each key step and wheel turn; event is details.event. Use it to save or fetch, where onChange fires on every step of a held button.
localeLocalesArgument, default The PerronProvider's locale
The locale the number is written and read in: "1,500" in English, "1.500" in Danish, "1 500" in Swedish and Norwegian. Left out, the nearest PerronProvider's.
placeholderstring, default —
Words shown in the empty box, such as "Such as 8". They vanish as the person types, so they never replace the label or the description.
increaseLabelstring, default The locale's
Overrides "Increase", the increment button's name.
decreaseLabelstring, default The locale's
Overrides "Decrease", the decrement button's name.
numberFieldLabelstring, default The locale's
Overrides "Number field", what a screen reader calls the input in place of its role (Owned strings).
refRef<HTMLDivElement>, default —
Reaches the root, the NumberField.Root <div>.
slotProps{ field?: Omit<FieldRootProps, "children" | "disabled" | "name"> | undefined; label?: FieldLabelProps | undefined; description?: FieldDescriptionProps | undefined; error?: FieldErrorProps | undefined; group?: NumberFieldGroupProps | undefined; input?: NumberFieldInputProps | undefined; decrement?: NumberFieldDecrementProps | undefined; increment?: NumberFieldIncrementProps | undefined; }, default —
Props for each part: field, label, description, error, group, input, decrement, increment.

Use it

import { NumberField } from '@mylder/perron'

Ask your agent

get_reference({ library: "base-ui", component: "NumberField" })

Keyboard and ARIA

From the Base UI reference for NumberField and the installed NumberFieldInput, NumberFieldRoot and useNumberFieldStepperButton. "From" says who does it. NumberField adds no keys.

KeyDoesFrom
Typing a digit or a separatorChanges the text and, once it parses as a number in the locale, the value, calling onChange ('input-change'); a character that can't be part of a number is not typedBase UI
ArrowUp, ArrowDownStep the value up or down by step, clamped to min and max, calling onChange and onChangeCommitted ('keyboard')Base UI
Shift + ArrowUp, ArrowDownStep by largeStep, 10 by defaultBase UI
Alt + ArrowUp, ArrowDownStep by smallStep, 0.1 by defaultBase UI
Home, EndSet the value to min or max, when it is set; without it, move the caret as the browser doesBase UI
Tab, Shift+TabMove focus to and from the input, the field's one tab stop; the buttons take none. Leaving marks the field touched; after typing, or when format rounds the number, even one set from outside by value, it also writes the number in the locale, clamps it to min and max unless allowOutOfRange is set, and calls onChangeCommitted. A change to value from outside while the input had focus is not committed on leaving, unless format rounds it. Leaving a read-only field only marks it touchedBase UI
EnterIn a form with a submit button, the browser clicks it and submits the form; the number is the hidden input's value. A form without one isn't submitted, since the hidden input is a second fieldBrowser
A press on a buttonSteps once, then every 60 ms while held past 400 ms, calling onChange on each step and onChangeCommitted on release; a mouse press focuses the inputBase UI
A press on the labelFocuses the input: natively through htmlFor, or through Base UI's own handler for a hidden labelBase UI
The restMove the caret, select, cut, copy and paste, as the browser's own <input> does; a paste is spliced in at the caret when the result parsesBrowser
ElementRoleAccessible name fromStates announced
rootnone: a <div>nonenone
groupgroup, from Base UInone: the input inside it carries the namenone
inputtextbox, native, described as the owned "Number field" by aria-roledescriptionaria-labelledby the label, and its native label through htmlFor: "Departures to show", or "Departures to show (optional)" with optionalIts value as text, formatted in the locale; aria-describedby the description, then the mounted error; aria-invalid while invalid and not disabled; required; disabled; readonly
decrementbutton, nativearia-label, the owned "Decrease", or decreaseLabelaria-disabled at min, while read-only and while disabled; aria-controls the input
incrementbutton, nativearia-label, the owned "Increase", or increaseLabelaria-disabled at max, while read-only and while disabled; aria-controls the input
iconnone: aria-hidden="true"nonenone
hidden inputnone: aria-hidden, tabIndex −1, Base UI'snonenone: out of the tree
fieldnone: a <div>nonenone
labelnone: a native <label>, or a hidden spanIts text: the caller's label, then "(optional)" with optionalnone
descriptionnone: a <p>noneRead as the input's description
errornone: a <div>noneRead as the input's description while it is mounted, with no live region of its own (Field's spec)

The buttons stay out of the tab order and in the accessibility tree, as Base UI keeps them: a keyboard steps the number with the arrow keys on the input, which says so through its role description, and a touch screen reader, which has no arrow keys, reaches the buttons by swiping.

What axe won't catch, from the contract, each with how this component meets it:

  • Everything the eye is told, the ear is told, for every shape each prop accepts: the label names the input, even hidden, with the optional mark inside it; the description and the error are the input's aria-describedby; an invalid field is aria-invalid and its error is words beside the red edge; each button's icon is aria-hidden and its name says what it does; a button the eye sees in the tertiary ink is aria-disabled for the ear, at a bound, read-only or disabled; the placeholder is never the name.
  • Every landmark and named region has an accessible name: none here; Base UI's group is no landmark, and a group of fields is a Fieldset's to name.
  • Every default label can be overridden: increaseLabel, decreaseLabel and numberFieldLabel override NumberField's three owned strings, and optionalLabel Field's.
  • Every shape the type accepts renders, in every variant: label, description and error render a string, an element and a list alike, as Field's do; value renders a number and an empty box for null; a format with a unit or a percent sign renders inside the box; a long Danish compound wraps in the label, the description and the error, and a long number stays on one scrolling line in the box (CR-10).
  • A hidden copy kept for animation is aria-hidden, and a collapsed region is inert: none here; the error is unmounted while the field is valid, and Base UI's hidden input is aria-hidden because the visible input is the control.
  • Native semantics first: a native <input>, a native <label> and two native <button>s; Perron adds aria-label on the buttons and aria-roledescription on the input, each in place of Base UI's English, and Base UI adds role="group", aria-labelledby, aria-describedby, aria-invalid, aria-controls and aria-disabled.
  • Focus moves in reading order and returns on close: the input is the field's one tab stop, between the label and description above it and the error below; a mouse press on a button leaves focus on the input; nothing opens or closes.
  • Every target is at least size.target-min square: each button is a size.control square, 34 px; the box is size.input tall and as wide as its container.
  • Loops stop when the system asks for less motion: none here; a held button repeats a step, which is no animation, and the box has no transition (CR-11).

Owned strings

The buttons' names are owned strings with the keys increase and decrease, in English "Increase" and "Decrease", set as each button's aria-label in place of Base UI's hard-coded English. Base UI names the buttons in English in every locale, so a Danish screen reader would hear "Increase", which the contract's Words forbids; "Increase" and "Decrease" are Base UI's own words, sentence case and plain, naming what the press does to the number whatever it stands for; and aria-label is how Base UI already names them, which expectOwnedStrings reads, where a VisuallyHidden span would first have to undo it.

The input's role description is an owned string, key numberField, in English "Number field", set as aria-roledescription in place of Base UI's hard-coded English rather than removed. Base UI sets aria-roledescription="Number field" in every locale, so a Danish screen reader would call the box a "Number field"; the description is what tells a screen reader user the arrow keys step it, since the buttons take no focus; and packages/perron/src/testing/owned-strings.ts reads no aria-roledescription today, so the build adds it to the attributes the check reads, with a test, or this word would pass the check in English on every screen (CR-9).

KeyEnglishOverrideSeen or heard where
increase"Increase"increaseLabelThe increment button's aria-label, heard as its name; never seen
decrease"Decrease"decreaseLabelThe decrement button's aria-label, heard as its name; never seen
numberField"Number field"numberFieldLabelThe input's aria-roledescription, heard in place of its role, after its name; never seen
optional"(optional)"optionalLabelField's optional span, seen after the label's words with optional, and heard in the input's name

increase, decrease and numberField are new keys in packages/perron/src/locale/en.ts, each with a JSDoc line saying where it is heard, and agents write their Danish, Swedish and Norwegian in the other locale files. optional is the key it already has, Field's: NumberField passes optional and optionalLabel to Field, which reads it. Every other word is the caller's, through label, description, error, placeholder and the units a format writes, or Base UI's message for a failing check: the caller's validate result, or the browser's own message for the hidden input's required, min, max and step, Field's known exception, which applies here unchanged (Field). The number's digits, separators and signs hold no letter, so expectOwnedStrings passes them in every locale; a unit a format writes, such as "min", is the caller's word, given in the story's caller strings.

Do and don't

For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.

RuleLevelDoDon'tChecked by
DD-1mustUse NumberField for a count or an amount a person may nudge up or down, such as the departures a board showsUse it for a number that is a name, such as a line number, a stop code or a phone number: no one steps those, and a separator or a dropped leading zero would change them; use TextFieldreview
DD-2shouldSet min and max wherever the number has bounds, and say them in description, "From 1 to 20"Leave the bounds to an error after the fact: the buttons and the keys clamp to them only when they are setreview
DD-3shouldPass the unit through format, such as { style: 'unit', unit: 'minute' }, so it is written in the locale with a space before itWrite the unit into label alone, or into a text beside the box: the number in the box would read barereview
DD-4mustRead the number from onChange's second argument, and save or fetch on onChangeCommittedRead event.target.value: it is the formatted text, "1,500", not the number; and don't fetch on every onChange, which fires on each step of a held buttonreview
DD-5mustConnect React Hook Form through Controller, with onChange={(event, value) => field.onChange(value)}Spread register() on NumberField: its value is a number Base UI keeps, not a native input's textreview
DD-6mustLet the PerronProvider's locale write the number, or give locale only where the number must be written in another languageFormat the number yourself and pass a string: value is a number, and Base UI parses what the person types in the same localetypes
DD-7mustGive error from the form library whenever the field is invalid, worded as what to do, "Enter a number from 1 to 20"Set slotProps.field.invalid with no message, or blame the person ("Invalid number"), against Content designreview
DD-8shouldSet the width on the container, or on the frame through slotProps.field, wide enough for the longest number and both buttonsSqueeze the box until the number scrolls: the buttons keep their size, and the number is what the person readsreview
DD-9mustUse NumberField on its own: it already has its label, its help and its errorWrap NumberField in Field: the input would be labelled twice, as Field's spec saysreview