BetaForms

NumberField

A number, typed — with the two ends of its range and a pair of buttons that walk it.

Overview

A number, typed — with the two ends of its range and a pair of buttons that walk it.

First example

<NumberField min={1} max={99} defaultValue={1} onValueChange={setQuantity}>
<NumberField.Label>Quantité</NumberField.Label>
<FieldGroup>
<NumberField.Decrement accessibilityLabel="Retirer un article" />
<NumberField.Field />
<NumberField.Increment accessibilityLabel="Ajouter un article" />
</FieldGroup>
</NumberField>

Anatomy

<NumberField>
  <NumberField.Decrement />
  <NumberField.Field />
  <NumberField.Increment />
</NumberField>

Usage

<NumberField min={1} max={99} defaultValue={1} onValueChange={setQuantity}>
  <NumberField.Label>Quantité</NumberField.Label>
  <FieldGroup>
    <NumberField.Decrement accessibilityLabel="Retirer un article" />
    <NumberField.Field />
    <NumberField.Increment accessibilityLabel="Ajouter un article" />
  </FieldGroup>
</NumberField>

Root props

NumberFieldProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
valuenumber | null | undefinedNoThe number shown. Present means controlled — the field follows it, and `onValueChange` is how it asks for a new one. `null` is an empty field.
defaultValuenumber | null | undefinedNoWhere an uncontrolled field starts.
onValueChange((value: number | null) => void) | undefinedNoEvery edit, with the number the field now holds — or `null` while what is in the box is not one yet. **While the reader is typing it is the raw number**, bounds and all: `min={10}` and a reader on their way to `15` types a `1` first, and clamping that to ten would take the keyboard away from them. The bounds are applied when they leave the field, and on every press of a stepper.
minnumber | undefinedNoThe floor. Unset, the field runs down as far as the reader takes it.
maxnumber | undefinedNoThe ceiling.
stepnumber | undefinedNoHow far one press of a stepper moves the value.
formatOptionsNumberFormatOptions | undefinedNoHow the value is written when the field is **not** being typed into — a currency, a unit, a fixed number of decimals. `Intl.NumberFormat`'s own options, unchanged. The moment the caret enters the box the value is rewritten plainly, without grouping, because a reader editing `1,234.50 €` should not have to type the comma or the euro sign back in.
localestring | undefinedNoWhich characters group and separate the number, in and out.
childrenReactNodeNo

Slots

NumberFieldDecrementProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
accessibilityLabelstring | undefinedNoWhat a screen reader says. There is no default, for the reason the close button's has none: a plus sign is not text, and the label beside it names the quantity rather than the action.
childrenReactNodeNoReplaces the drawn mark — an icon of your own.

NumberFieldFieldProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

NumberFieldIncrementProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
accessibilityLabelstring | undefinedNoWhat a screen reader says. There is no default, for the reason the close button's has none: a plus sign is not text, and the label beside it names the quantity rather than the action.
childrenReactNodeNoReplaces the drawn mark — an icon of your own.

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

The source defines no extra rule. Keep the React Native labels, roles, states and focus order your case needs, then check the result with VoiceOver and TalkBack.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

It is a `TextField`

The root is the TextField's root, unchanged: the same recipe, the same four variants, the same size, radius, color, labelPlacement, isInvalid and isDisabled. NumberField.Label, .Description and .Error are the TextField's slots — the same components, not wrappers. Only the field differs, by reading a number out of what is typed into it. The MaskField's arrangement exactly.

The value is a number, and the box is a string

Two representations, and the caret is what swaps them.

where the caret iswhat the box holds
outsidethe value written by Intl, formatOptions's way
insidethe same number written plainly, no grouping

Out of the field, formatOptions is Intl.NumberFormat's own options, unchanged — a currency, a unit, a fixed number of decimals cost nothing here.

Into it, everything that is not a digit, a sign or the decimal mark is dropped rather than refused. The value the caret lands in is grouped, and rejecting its separators would make the first keystroke on an existing value clear the box. A full stop passes as the decimal mark wherever the locale is not using it to group, so 1.5 reads as one and a half in fr-FR and 1.234 still reads as a thousand-odd in de-DE.

On the way in the value is rewritten plainly, so nobody has to type a euro sign back in.

null, never NaN: "not a number yet" is the state a half-typed field is in for as long as it takes to type a minus sign, and NaN is a value that propagates instead of a state a caller can test.

The bounds land when the reader leaves, not while they type

min={10}, and a reader on their way to 15 types a 1 first. Clamping that would take the keyboard away from them.

So until the field is left, onValueChange reports what is actually in the box. The clamp falls on blur, and on every press of a stepper.

The steppers

They are FieldGroup decorators, like TimeField.Period — that is the thing that lays a control over a field and measures it, so the box stays the TextInput itself and no wrapper borrows its border, its fill and its radius.

Decrement takes the leading edge and Increment the trailing one. The value sits between them, which is the only arrangement in which the two read as one control rather than as two marks that happen to be nearby. start and end, never left and right (R13).

A button goes flat when the value has nowhere left to go, and stops taking presses there. That is asked of the result rather than of the bound: a value half a step short of the ceiling can still reach it, and a button dead at that point strands the reader.

With no children each draws its own mark — one bar, or two a quarter turn apart — so the field works in a project that has installed no icon set. Pass an <Icon> to replace it.

From an empty field, a step starts at zero

Which is what puts the first press on min when the range starts above it: min={5} and a step of one gives 5, not 6.

The keyboard is a guess, and a guess you can override

A fractional step, or a formatOptions that asks for decimals, opens the decimal pad; anything else opens the number pad. Unlike the MaskField's, NumberField.Field does not claim keyboardType — a field that has to take a minus sign needs a keyboard with one on it, and that is the caller's call.

Related

  • NumberStepper — the same pair without a field to type into.
  • Slider — a quantity chosen on a track.
  • TextField — the root, the label, the hint and the error.