Input OTP
StableA segmented field for one-time passcodes, PINs and recovery codes, with paste, SMS autofill and password-manager support.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/input-otp.jsonUsage
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Input OTP is built on input-otp: the slots are a picture of one real input underneath. That is why paste, SMS autofill, password managers, undo and screen readers all work — there's a single field to fill, not six.
Examples
Digits only
Pass a pattern to reject characters as they're typed. REGEXP_ONLY_DIGITS fits PINs and SMS codes, and the default inputMode="numeric" brings up the number pad on phones. The patterns are re-exported from @/components/ui/input-otp; like the component, use them in client components.
Letters and symbols are ignored as you type.
Letters and digits
For recovery codes, allow letters with REGEXP_ONLY_DIGITS_AND_CHARS and switch inputMode to text so mobile keyboards can type them. A pasteTransformer cleans pasted values — here it strips dashes and uppercases the code.
Paste a code like 7F2K-9QXA — dashes are removed.
Controlled with auto-submit
Control the value with value and onChange, and verify with onComplete, which fires once every slot is filled. Disable the field while verifying, keep what people typed if it fails, and say where the code was sent.
We sent it to ada@prfct.dev.
Invalid
Set aria-invalid on InputOTP and every slot turns red — there's no need to mark slots one by one. Explain the problem in a FieldError linked with aria-describedby.
Guidelines
When to use
- Fixed-length codes people copy from somewhere else: two-factor codes, email verification codes, PINs, recovery codes.
When not to use
- Passwords or anything of variable length — use an Input with
type="password". - Codes longer than about 8 characters — a single input with a clear format hint is easier to fill.
Chunk long codes
Short-term memory holds three or four characters at a time. Split six-digit codes into two groups of three and eight-character codes into two groups of four with an InputOTPSeparator — and match the grouping of the email or SMS that delivers the code.
Forgive and recover
Submit automatically on completion, but never lock people in: keep their input when a code fails, let them edit any slot, and offer Resend code with a visible cooldown rather than a dead end.
Accessibility
- One field. Screen readers announce a single text field and read back the full value; there is one tab stop, not one per slot.
- Label it. Give
InputOTPanidfor aFieldLabel, or anaria-labelsuch as Verification code. - Autofill.
autoComplete="one-time-code"is set by default, so iOS and Android offer codes from SMS and email. - Password managers. With
pushPasswordManagerStrategy="increase-width"(the default), the input grows invisibly so manager badges don't cover the last slot. - Contrast. Slot borders use the
inputtoken at 3:1; the active slot adds the brand focus halo.
| Key | Behavior |
|---|---|
Tab | Moves focus to the field. The caret lands in the first empty slot. |
0–9 / A–Z | Fills the active slot and advances. Characters that don’t match pattern are ignored. |
Backspace | Deletes the previous character. |
←→ | Moves the caret between slots. |
CtrlV⌘V | Pastes a code and distributes it across the slots. |
API reference
InputOTP
Renders the container and one hidden <input>. Accepts all native input props plus the options of input-otp.
InputOTPGroup
Renders a <div> that joins its slots into one bordered segment.
InputOTPSlot
Renders one character cell. Accepts all div props.
InputOTPSeparator
Renders a <div role="separator"> with a dash between groups.