NumberPad
The keypad a PIN, a code and an amount are typed on.
Overview
The keypad a PIN, a code and an amount are typed on.
First example
<NumberPad maxLength={4} onComplete={unlock} />
Anatomy
<NumberPad>
<NumberPad.Action />
<NumberPad.Backspace />
<NumberPad.Icon />
<NumberPad.Key />
<NumberPad.Label />
</NumberPad>
Usage
<NumberPad maxLength={4} onComplete={unlock} />
<NumberPad maxLength={4} value={pin} onChangeText={setPin}>
<NumberPad.Action onPress={faceId} accessibilityLabel="Unlock with Face ID">
<NumberPad.Icon as={FaceIdIcon} color={theme.colors.accent} />
</NumberPad.Action>
</NumberPad>
Root props
NumberPadProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | NumberPadVariant | undefined | No | — |
| size | NumberPadSize | undefined | No | The key's height, its corner, the gaps and the digit. Never a width. |
| radius | RadiusKey | undefined | No | Overrides the corner `size` chose. |
| color | string | undefined | No | A raw tint (R7), landing where the variant put its tokens — the filled keys only. The backspace and the free cell keep the page's foreground, because a bare glyph on a tinted pad has no tinted ground to read against. |
| value | string | undefined | No | Controlled. Leave it out and the pad keeps the value itself. |
| defaultValue | string | undefined | No | The starting value when uncontrolled. |
| onChangeText | ((value: string) => void) | undefined | No | — |
| onComplete | ((value: string) => void) | undefined | No | Fired once the value reaches `maxLength` — the PIN screen's only event. |
| maxLength | number | undefined | No | How long the value may get. Unset it is unbounded, which is what a pad in front of an amount wants; set, a press past it changes nothing at all rather than truncating. |
| isDisabled | boolean | undefined | No | Dims every key and stops all of them. |
| children | ReactNode | No | **One cell**, and it fills the free corner of the bottom row — beside the `0` and opposite the backspace. `NumberPad.Action` is what usually goes there: a fingerprint, a decimal separator, a `Clear`. The digits and the backspace are not composed, because they are not a decision (see `NUMBER_PAD_ROWS`). Left out, the corner is an empty cell that keeps the `0` centred. |
| style | StyleProp<ViewStyle> | No | — |
Slots
NumberPadActionProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| isDisabled | boolean | undefined | No | R8: `disabled` is not part of the public vocabulary, `isX` is. Overrides the pad's own — a single key can be out while the rest are live. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
| children | ReactNode | No | — |
NumberPadBackspaceProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| isDisabled | boolean | undefined | No | R8: `disabled` is not part of the public vocabulary, `isX` is. Overrides the pad's own — a single key can be out while the rest are live. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
| children | ReactNode | No | — |
NumberPadIconProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
NumberPadKeyProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| isDisabled | boolean | undefined | No | R8: `disabled` is not part of the public vocabulary, `isX` is. Overrides the pad's own — a single key can be out while the rest are live. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
| children | ReactNode | No | — |
| value | string | Yes | What this key inserts. Usually the digit it shows, which is also what it renders when nothing is composed inside it. |
NumberPadLabelProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Appearance
Five emphasis levels and no intent among them — there is no danger keypad, and success
and warning are outcomes rather than something you press.
default is what the pad ships as, where the Button ships as primary: eleven keys in the
accent is a wall of colour, and the digit is what the eye is looking for.
size | key height | gap | corner | digit |
|---|---|---|---|---|
sm | 60 | 8 | 18 | 24 |
md | 72 | 10 | 18 | 30 |
lg | 84 | 12 | 24 | 36 |
The height is a control and a half, derived from controlHeights rather than written out,
so a theme that raises its controls raises its keypad with them. A key is hit with a thumb
rather than pointed at, and it is hit eleven times in a row — a control-height key turns a PIN
into an exercise in precision. xs is absent for the same reason: 48 points holding a
24-point digit is a control pretending to be a key.
The gap is one value for both axes. A grid whose rows sit closer than its columns reads as three separate rows rather than as one pad.
No width anywhere in the recipe. Every cell is flex: 1, so the pad is as wide as it is
given and the three columns divide that between them.
Accessibility
Each cell is a button, labelled with what it inserts.
Not keyboardkey, which is the role that exists for exactly this and would have a screen
reader announce "1, key". React Native maps it to a trait on iOS only: on Android it falls
through to no role, and the web renderer emits a bare focusable div with no role at all. A
key that announces as nothing on two platforms out of three is worse than one that announces
as a button on all three. It is still only the default, so an iOS-only app passes
accessibilityRole="keyboardkey" on the cell.
The root is none. There is no keypad role in React Native, and reaching for toolbar or
list would have a screen reader announce the box as something it is not.
Migration from legacy
Implementation notes
The grid is data, not markup
1 through 9, the 0 and the backspace are not a decision a caller makes, so the root
renders them. Eleven hand-written cells is a layout that disagrees with itself the moment one
of them is edited, and there is no maxLength-shaped prop driving the count the way there is
in InputOTP — which is the one component here whose children genuinely are data and which
therefore takes a render function.
What is composed is the one free corner of the bottom row, opposite the backspace, and
that is what children is. One cell. Left out, the corner is still a cell — without it the
0 slides to the start of its row and stops being the middle column.
NUMBER_PAD_ROWS is exported, for a caller building a different pad out of the same cells.
It draws no display
What the value looks like is the screen's — an InputOTP's boxes, a row of dots, an amount in
a Typography. A pad that also rendered the value would be two components that have to agree
on a string, and the interesting half of that pair is always the one the pad did not
anticipate.
A masked display is a composition, not a prop
There is no isSecure here, and there does not need to be. InputOTP.Value takes children
that win over the box's own character, so a PIN masks in one line:
<InputOTP maxLength={6} value={pin}>
<InputOTP.Group>
{({ slots }) =>
slots.map(slot => (
<InputOTP.Box key={slot.index} index={slot.index}>
<InputOTP.Value>{slot.char ? '•' : undefined}</InputOTP.Value>
</InputOTP.Box>
))
}
</InputOTP.Group>
</InputOTP>
The lock screen's bare row of dots — no boxes at all — is a View per character filled up to
value.length, which is six divs and stays the screen's. A component for it would have to
pick a size, a gap, a colour and a shape on behalf of every lock screen that installs it.
Both are on the demo screen.
The value
One string, controlled or not: value / defaultValue / onChangeText, as everywhere in the
library. onComplete fires the moment it reaches maxLength — the PIN screen's only event.
maxLength clamps rather than truncating. A press past the limit changes nothing at all,
so onChangeText does not fire and a full PIN cannot be completed twice by leaning on a key.
The insert is measured whole rather than as one character, because a key may carry more than
one: a 00 key on a currency pad that landed halfway over the limit would produce a value one
character longer than the pad promised.
Unset, maxLength is unbounded — which is what a pad in front of an amount wants.
The cells
NumberPad.Key— one filled key.valueis what it inserts, and what it shows when nothing is composed inside it. Exported for the eleventh key: a decimal separator, a00.NumberPad.Backspace— the delete key, bare. A long press clears the whole value, the gesture every platform's own keypad carries; without it a mistyped sixteen-digit card is sixteen presses to undo.NumberPad.Action— the free corner. It does nothing on its own, because what belongs there is the caller's: a fingerprint that unlocks, aClear, a decimal separator.NumberPad.LabelandNumberPad.Icon— the character or the glyph on a cell.
A cell owns its press state
The root cannot see which of eleven keys is down, so it resolves both faces of the cell
style and each cell picks. That is the Menu's arrangement, for the same reason: nothing
re-resolves per key, so a pad of eleven costs what a pad of two would. R5 stays intact —
no slot touches the recipe.
Bare cells read the page's foreground
NumberPad.Label and NumberPad.Icon need no prop to know which colour they take: the cell
they are in publishes it. A filled key reads the variant's foreground; a bare corner reads
the page's.
That distinction is load-bearing rather than cosmetic. A primary pad puts
accentForeground on its digits, and a backspace with no ground of its own would take white
on white. It is also why a raw color does not reach the bare cells — a glyph with nothing
tinted behind it has nothing tinted to read against.
The backspace arrow is a character
←, not an icon, so the pad needs no react-native-svg for the one glyph it draws itself. It
flips to → under RTL, because a backspace points at what it removes and that is the other
way round in a right-to-left layout. Compose a NumberPad.Icon to replace it.
See also
InputOTP— the boxes this pad usually types into.NumberField— the same value with a keyboard instead of a pad.