VerificationCode
A segmented one-time-code input — one cell per digit, with auto-advance, paste distribution, arrow-key navigation, and OTP autofill support built in.
Overview
VerificationCode renders a configurable number of single-character cells (count, default 6) backed by a single controlled value string. Typing a character auto-advances focus to the next cell; Backspace/Delete clear a cell and move focus back; ArrowLeft/ArrowRight move between cells without altering their contents; and pasting a full code distributes its characters across the cells starting from whichever one is focused.
The component also wires up a visually hidden input that mirrors the joined value, so native HTML validation (required, pattern) works out of the box — a failed validation automatically falls back to an error status if none was explicitly set. The first cell sets autoComplete="one-time-code" so mobile browsers can offer SMS OTP autofill.
Import
import { VerificationCode } from '@unflow.io/ui/components/VerificationCode';Props
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 6 | Number of digit cells rendered |
separatorAt | number | 0 | Index after which a visual separator is inserted, e.g. separatorAt={3} with count={6} groups the cells as 123 · 456. 0 disables the separator |
value | string | — | Controlled value; each character maps to one cell |
onChange | (e: ChangeEvent<HTMLInputElement>) => void | — | Fired whenever any cell changes; e.target.value is the full joined code |
onComplete | (e: ChangeEvent<HTMLInputElement>) => void | — | Fired once every cell has a character |
onBlur | (e: FocusEvent<HTMLInputElement>) => void | — | Fired when focus leaves the whole group (not when it moves between cells) |
label | string | — | Optional label rendered above the group via InputLabel |
hint | string | — | Optional helper text rendered below the group |
status | 'valid' | 'invalid' | 'error' | — | Visual status applied to every cell (border/focus-ring color). Set automatically to 'error' when native validation fails and no explicit status is passed |
disabled | boolean | false | Disables every cell |
required | boolean | — | Marks the field required for native form validation (enforced via the hidden mirror input) |
disableRequiredSymbol | boolean | — | Hides the required symbol next to the label |
name | string | — | Name of the hidden input submitted with the surrounding <form> |
pattern | string | — | Regex used to accept/reject characters while typing or pasting (defaults to alphanumeric) |
inputMode | string | 'text' | inputMode applied to every cell — use 'numeric' to prefer a numeric keyboard on mobile |
placeholder | string | '0' | Placeholder shown in each empty cell |
autoFocus | boolean | — | Focuses the first empty cell (or the last cell if all are filled) on mount |
onInvalid | (e: FormEvent<HTMLInputElement>) => void | — | Fired on native validation failure (e.g. required with an incomplete value) |
className | string | — | Classes merged into every cell's built-in styles |
Basic usage
const [value, setValue] = useState('');
<VerificationCode
name="verification-code"
label="Verification code"
hint="Enter the 6-digit code we sent to your phone."
value={value}
onChange={(e) => setValue(e.target.value)}
/>Grouped digits
Use separatorAt to visually split the cells into groups, e.g. a 6-digit code shown as two groups of three.
<VerificationCode
name="verification-code-grouped"
label="Verification code"
count={6}
separatorAt={3}
value={value}
onChange={(e) => setValue(e.target.value)}
/>Validation status
Pass status to flag the code as invalid or in error, independently of native validation.
<VerificationCode
name="verification-code-status"
label="Verification code"
hint="That code is invalid or has expired."
status="error"
value={value}
onChange={(e) => setValue(e.target.value)}
/>Disabled
<VerificationCode
name="verification-code-disabled"
label="Verification code"
value="123456"
disabled
/>Customization (slots & slotProps)
| Slot | Type | Description |
|---|---|---|
separator | ReactNode | Overrides the rendered separator entirely; when provided, slotProps.separator no longer applies |
| Slot Prop | Props | Description |
|---|---|---|
inputLabel | Omit<IInputLabel, 'label' | 'hint' | 'required' | 'disableRequiredSymbol' | 'children'> | Props forwarded to the underlying InputLabel wrapper |
inputContainer | BaseHTMLProps<HTMLDivElement> | Props for the role="group" container wrapping the cells |
separator | BaseHTMLProps<HTMLSpanElement> | Props for the default separator element |
Accessibility
- The cell group is a
role="group"element witharia-label={label}. - Each cell has its own accessible name,
aria-label="Digit N of {count}". aria-invalidis set on every cell whenstatusis'error'or'invalid'.aria-describedbylinks each cell to thehinttext when one is provided.- A visually hidden input mirrors the joined value and drives native
required/patternvalidation, surfacing the browser's validation message as anerrorstatus automatically. - Full keyboard support: typing auto-advances focus, Backspace/Delete clear a cell and move focus back, ArrowLeft/ArrowRight move between cells, and Enter submits the enclosing form.
- Pasting a full code into any cell distributes it across the remaining cells starting from that position.