Skip to main content

PinInput

A pin input collects a one-time password or PIN. Each cell is a native <input> labelled "Digit n of N"; the root is a role="group". The value behaves like one text field: Backspace in a filled cell deletes it and shifts later cells left, in an empty cell it deletes the previous one and moves back.

import PinInput from '@zuilib/components/pin-input'

Basic

onChange fires with the typed prefix on every edit; onComplete fires once, when the value becomes length long. otp marks the cells autocomplete="one-time-code" so the OS can offer a received SMS code.

Loading example

Types, mask and placeholder

numeric (the default) accepts digits and opens the numeric keypad; alphanumeric accepts letters too. mask obscures the characters with -webkit-text-security: disc while the cells stay type="text", so inputmode, autofill and password managers keep behaving.

Loading example

Sizes

Cells are squares the height of the control of the same size.

Loading example

In a form item

Inside a FormItem the Label names the group and focuses the first cell, the item's Description and Message join every cell's aria-describedby, and invalid / disabled are inherited. name adds a hidden input so the joined value submits with a native form.

Loading example

Props

PinInputProps extends React.HTMLAttributes<HTMLDivElement> (minus onChange, defaultValue, placeholder) with:

PropTypeDefaultDescription
lengthnumber6Number of cells
valuestringControlled value: the typed characters in order, at most length long
defaultValuestring''Uncontrolled initial value
onChange(value: string) => voidEvery edit, with the joined (gap-free) value
onComplete(value: string) => voidOnce, when the value becomes complete
type'numeric' | 'alphanumeric''numeric'Accepted characters; numeric also sets inputmode="numeric" and pattern="[0-9]*"
maskbooleanfalseObscures the cells with -webkit-text-security: disc
placeholderstringOne character shown in an empty cell
invalidbooleanFormItem invalidaria-invalid / data-invalid on the cells and the destructive colour
disabledbooleanFormItem disabled
namestringRenders a hidden input with the joined value for native form submission
formstringThe hidden input form attribute
otpbooleanfalseautocomplete="one-time-code" on the cells
autoFocusbooleanfalseFocuses the first cell on mount
idstringField control idGoes on the first cell (what a Label htmlFor focuses)
cellLabel(index: number, length: number) => string'Digit n of N' / 'Character n of N'Accessible name of each cell
size'sm' | 'md' | 'lg''md'
classNamestringMerged last onto the root
cellClassNamestringMerged last onto every cell

Slots

SlotElementNotes
[data-slot="pin-input"]divRoot (role="group"), with data-size, data-type, data-state="default|error", data-invalid, data-disabled, data-complete, data-masked
[data-slot="pin-input-cell"]inputOne per character, with data-index, data-state, data-filled, data-invalid, data-disabled
[data-slot="pin-input-hidden"]input[type=hidden]Present when name is set
/* Consumer CSS: tint filled cells */
[data-slot="pin-input-cell"][data-filled] {
background-color: var(--accent);
}

Tokens

TokenUsed for
--control-height-sm/md/lgCell width and height per size
--input, --background, --foregroundCell border, surface and text
--ring, --destructiveFocus ring, error state
--muted-foregroundPlaceholder
--duration-fastColour transition

Accessibility

  • The root is role="group". Inside a FormItem the item's Label names it through aria-labelledby and its htmlFor focuses the first cell; outside one, pass aria-label or aria-labelledby.
  • Every cell has its own name (Digit 3 of 6), aria-describedby from the item's Description / Message, and aria-invalid when invalid.
  • Keyboard: a character fills the cell and moves on; Backspace deletes (and moves back from an empty cell); Delete clears the cell; ArrowLeft / ArrowRight, Home / End move between cells. Focusing a cell selects its content, so typing over a filled cell replaces it.
  • Paste, IME commits and autofill of any length spread from the focused cell on; characters the type rejects are dropped.
  • Disabled cells use the native attribute.
  • Input: the single-field text control.
  • NumberInput: for numbers rather than codes.
  • FormItem: label, description and message wiring.