Components · Forms
OTPPinInput
One-time code or PIN entry in separate boxes, backed by one accessible input.
Verify a code
Try 123456.
import { useState } from 'react';
import { Button, Field, FieldDescription, FieldError, FieldLabel, OTPPinInput } from '@rookcom/monarch-react';
function VerifyDemo() {
const [code, setCode] = useState('');
const [state, setState] = useState<'idle' | 'checking' | 'wrong' | 'ok'>('idle');
const verify = (value: string) => {
setState('checking');
// Demo only: the product verifies the code on its server.
setTimeout(() => setState(value === '123456' ? 'ok' : 'wrong'), 500);
};
return (
<div style={{ display: 'grid', gap: 12, width: '100%' }}>
<Field invalid={state === 'wrong'} disabled={state === 'checking'}>
<FieldLabel>Verification code</FieldLabel>
<OTPPinInput
value={code}
onValueChange={(v) => {
setCode(v);
if (state === 'wrong') setState('idle');
}}
onComplete={verify}
/>
<FieldDescription>Try 123456.</FieldDescription>
<FieldError>
{state === 'wrong' ? 'That code is not right. Check it and try again.' : null}
</FieldError>
</Field>
<div>
<Button
variant="primary"
loading={state === 'checking'}
disabled={code.length !== 6}
onClick={() => verify(code)}
>
{state === 'ok' ? 'Verified' : 'Confirm'}
</Button>
</div>
</div>
);
}
<VerifyDemo />Usage examples
PIN and groups
import { OTPPinInput } from '@rookcom/monarch-react';
<div style={{ display: 'grid', gap: 16, width: '100%' }}>
<OTPPinInput aria-label="4-digit PIN" length={4} mask defaultValue="25" />
<OTPPinInput aria-label="Grouped code" groupSize={3} defaultValue="4829" />
<OTPPinInput
aria-label="Recovery code"
length={8}
mode="alphanumeric"
groupSize={4}
defaultValue="K7P2"
/>
</div>Purpose
Enters a short numeric or alphanumeric code (an email or SMS one-time code, a step-up PIN) shown as one box per character, while remaining a single real input for autofill, paste, mobile keypads and screen readers.
Use when
- Sign-in or step-up verification with a code sent to the user.
- A short PIN where the box per character shows how many are needed.
Avoid when
- Passwords or long secrets: use a password Input.
- Codes of variable length: use Input.
Design principles
- One field, many boxes: the boxes are a picture of one
<input>, so the platform features for one-time codes just work. - Forgiving entry: spaces, dashes and other characters in a pasted or typed code are dropped.
- Verification is the product’s:
onCompletehands over the code, and the product shows the result through Field.
API reference
import { OTPPinInput } from '@rookcom/monarch-react';| Export | Kind | Rendering | Root class |
|---|---|---|---|
OTPPinInput | component | client | .mn-otp-pin-input |
Renders .mn-otp-pin-input with aria-hidden slots and a transparent <input> laid over them. The ref and native input props go to the input; className goes to the wrapper.
Reads Field context through useFieldControl for its label, description, invalid and disabled.
Generated from the TypeScript declarations of packages/react/src/components/forms/otp-pin-input.
Properties
maxLength, minLength and pattern are set by the component from length and mode and cannot be passed.
OTPPinInput
A one-time code or PIN entry shown as separate boxes. It is one real `<input>` underneath, so autofill from SMS or email (`autocomplete="one-time-code"`), paste, the numeric keypad and screen readers all treat it as a single field. Give it a label with Field or `aria-label`.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | |
className | string | Class for the outer wrapper (the real input is `.mn-otp-pin-input__input`). |
defaultValue | string | Initial code when uncontrolled. |
disabled | boolean | |
groupSize | number | Draw a gap after every N slots, e.g. 3 for "123 456". Visual only. |
id | string | |
invalid | boolean | Marks the code invalid (e.g. after a failed verification). Inside a Field, the Field's `invalid` is used. |
length | number | Number of characters. Default 6. |
mask | boolean | Show dots instead of the characters, for a PIN typed on a shared screen. |
mode | "numeric" | "alphanumeric" | `numeric` (default) accepts digits and opens the number keypad; `alphanumeric` accepts A–Z and 0–9, upper-cased. |
onComplete | ((value: string) => void) | Called once each time the code reaches `length` characters (typed, pasted or autofilled). |
onValueChange | ((value: string) => void) | Called with the cleaned code on every change. |
size | "md" | "lg" | |
value | string | The code (controlled). |
Variants
| Prop | Values | Description |
|---|---|---|
length | 'number (default 6)' | Number of boxes and characters. |
mode | 'numeric''alphanumeric' | numeric keeps digits and opens the number keypad; alphanumeric keeps A–Z and 0–9, upper-cased. |
mask | 'boolean' | Dots instead of characters; the input becomes type="password". |
groupSize | 'number' | A visual gap after every N boxes (e.g. 3 for "123 456"). |
size | 'md''lg' | Box size. Default md. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Focused | data-focused | The next box to fill shows the focus ring and a blinking caret. |
| Filled | data-filled on a slot | The box shows its character or a dot. |
| Complete | data-complete | Every box is filled; onComplete has fired. |
| Invalid | data-invalid, aria-invalid="true" | Danger borders, from invalid or the Field. |
| Disabled | data-disabled, disabled | Muted boxes, for example while verifying. |
Interaction behaviour
- Typing fills the boxes in order; characters that do not fit the
modeare dropped. - Pasting a full code replaces the value; pasting a fragment appends it. Spaces and dashes are removed.
- Autofill from SMS or email (
autocomplete="one-time-code") fills every box at once. onCompletefires each time the code reacheslengthcharacters, whether typed, pasted or autofilled.- The caret always stays at the end, so editing is by Backspace; clicking any box focuses the field.
- Controlled with
value+onValueChange(set""to reset after a failed attempt), or uncontrolled withdefaultValue.
Keyboard support
| Keys | Action |
|---|---|
| Characters | Fill the next box. |
| Backspace | Clears the last filled box. |
| Ctrl/⌘ + V | Pastes a code. |
| Tab | Moves focus in and out as one field. |
Accessibility
- Role
- textbox (one native input)
- Accessible name
- A FieldLabel or
aria-labelsuch as "6-digit code". Screen readers hear one field and its value, not six unnamed boxes.
- The visual boxes are
aria-hidden. - Errors and help come from Field and are linked with
aria-describedby. inputmode="numeric"andpattern="[0-9]*"open the number keypad on phones without blocking paste.- With
mask, the input istype="password", so assistive technology does not read the PIN aloud.
Responsive behaviour
- Boxes are 2.75rem wide (3.25rem at
lg) and shrink evenly when the container is narrower, down to 1.5rem each, so even an 8-character grouped code fits a 320px screen. In a shrink-to-fit container (inline orwidth: max-content) give the field a width, or the boxes stay at that minimum. - The input font size is 16px, which keeps iOS from zooming on focus.
Theme support
- Box borders, surfaces and the focus ring are semantic tokens, so light, dark and more-contrast themes apply.
- The caret blink and box transitions stop under
prefers-reduced-motion.
Do and don't
Do
- Verify in
onCompleteand also offer a Confirm button for users who type slowly. - Disable the field (through Field) while the code is verified, and clear it after a wrong code.
- Tell the user where the code was sent and how many characters it has.
Don't
- Do not build one input per box; autofill and screen readers break.
- Do not reveal whether a code was close; just say it was not right.
Related components
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Field |
Testing coverage
- One labelled input with
one-time-code, numeric keypad and pattern; slots are aria-hidden. - Typing fills slots, drops other characters, and
onCompletefires once at full length. - Backspace deletes.
- Pasting a full code (with spaces and dashes) replaces; a fragment appends.
- Other lengths, alphanumeric mode with upper-casing, and groups.
- Mask shows dots and uses a password input.
- Controlled mode and reset by the parent.
- Reads Field label, error, invalid and disabled.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/otp-pin-input |
Tests: packages/react/src/components/forms/otp-pin-input