PinField
Introduction
A segmented input for short fixed-length codes — one-time passwords, SMS/email
verification codes, PINs. Each character gets its own box, with keyboard
navigation, paste/autofill support, and the same label/helper text/error
text/validator conventions as Field.
Usage
import { PinField } from "../components/ui";
export default function MyPage() {
return (
<PinField
label="Verification code"
helperText="Check your email for the 6-digit code"
count={6}
otp
blurOnComplete
onValueComplete={(details) => console.log(details.valueAsString)}
/>
);
}
Character format
format ("numeric" | "alphanumeric" | "alphabetic", default "numeric")
restricts what a box accepts and picks the right mobile keyboard via
inputMode. Pass pattern with a custom per-character regexp source to
override it entirely — useful for coupon-style codes.
<PinField format="alphanumeric" count={8} placeholder="_" />
Autofill
otp marks the field as an SMS/email one-time code. Rather than putting
autocomplete="one-time-code" on every box (which makes browsers and password
managers show a fill affordance on all of them at once), only the box the user
would fill next advertises it — and accepts the full code length, so a mobile
keyboard's OTP suggestion or a full paste can land in one shot and get
redistributed across the remaining boxes automatically. Every other box opts
out of autofill and password-manager icon overlays.
Keyboard and pointer behaviour
- Typing a valid character fills the current box and advances focus; an
already-focused box's content is selected first (
selectOnFocus, on by default), so typing always replaces rather than getting silently blocked. - Backspace on an empty box clears and steps back into the previous one.
- Arrow Left/Right move focus between boxes; Arrow Up/Down are suppressed rather than doing nothing useful.
- Pasting or an OS/password-manager autofill distributes characters across boxes starting from the one it landed on.
- Tab and click/pointer focus stop at the first empty box — you can't jump ahead of a gap, only edit an already-filled box or continue where you left off.
Validation
Like Field, PinField accepts a validator that runs against the joined
value and can return false (generic error) or a string (custom message).
It only re-validates once every box is filled.
<PinField
count={6}
validator={(value) =>
value !== "000000" || "That code isn't valid"
}
errorText="Enter the code we sent you"
/>
Forms: auto-submit and reset
form associates the field's hidden submit input with a <form id> elsewhere
in the document (or omit it if the field already lives inside the form).
autoSubmit calls form.requestSubmit() the moment every box is filled,
after firing onAutoSubmit; pressing Enter in any box also attempts a
submit, since a form with several sibling inputs suppresses the browser's own
submit-on-Enter. Resetting the form (a native <button type="reset">, or
form.reset()) clears the field back to empty.
<form id="verify-form">
<PinField form="verify-form" name="code" count={6} otp autoSubmit />
<button type="reset">Clear</button>
</form>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 4 | Number of boxes. |
value | string[] | - | Controlled value, one entry per box (forces interactive mode). |
defaultValue | string[] | - | Initial value, one entry per box (forces interactive mode). |
format | "numeric" | "alphanumeric" | "alphabetic" | "numeric" | Character class accepted per box. |
pattern | string | - | Custom per-character regexp source, overrides format. |
placeholder | string | "○" | Shown in each empty box. |
mask | boolean | - | Renders each box as type="password". |
otp | boolean | - | Targets autocomplete="one-time-code" autofill at the active box only. |
blurOnComplete | boolean | - | Blurs the last box once every box is filled. |
autoFocus | boolean | - | Focuses the first box on mount. |
selectOnFocus | boolean | true | Selects a box's content on focus so typing replaces it. |
disabled | boolean | - | Disables every box. |
readOnly | boolean | - | Prevents editing. |
required | boolean | - | Marks every box as required. |
invalid | boolean | - | Forces the invalid state, overriding validator. |
name | string | - | Form field name for the hidden submit input. |
form | string | - | Associates the hidden input (and autoSubmit/reset) with a <form id>. |
autoSubmit | boolean | - | Calls form.requestSubmit() once complete (forces interactive mode). |
onAutoSubmit | (valueAsString: string) => void | - | Fired right before an autoSubmit submission is attempted. |
label | Child | - | The field's label. |
helperText | Child | - | Helper text shown below the boxes. |
errorText | Child | - | Error text shown when invalid; a validator string result overrides this. |
validator | (value: string) => boolean | string | - | Validates the joined value once complete (forces interactive mode). |
onValueChange | (details: { value: string[]; valueAsString: string }) => void | - | Fires on every change (forces interactive mode). |
onValueComplete | (details: { value: string[]; valueAsString: string }) => void | - | Fires once every box is filled. |
onValueInvalid | (details: { index: number; value: string }) => void | - | Fires when a typed/pasted character is rejected by format/pattern. |
interactive | boolean | - | Forces or forbids hydration as an island. |