Skip to content

Input OTP

Stable

A 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.json

Usage

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 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.

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.

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.

4
8
1
2
0
9

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.

Do.Two groups of three mirror how people read and remember the code.
Don’t.Separate inputs per digit break paste, autofill and screen readers.

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 InputOTP an id for a FieldLabel, or an aria-label such 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 input token at 3:1; the active slot adds the brand focus halo.
KeyBehavior
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.
CtrlVV
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.

PropTypeDefault
maxLengthrequired

Number of characters — must match the number of slots.

numberNo default
value

The controlled value.

stringNo default
onChange

Called with the new value on every change.

(value: string) => voidNo default
onComplete

Called when every slot is filled.

(value: string) => voidNo default
pattern

A regular expression each character must match. Use the exported REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS or REGEXP_ONLY_DIGITS_AND_CHARS.

stringNo default
pasteTransformer

Cleans pasted text before it's validated, e.g. removing dashes.

(pasted: string) => stringNo default
inputMode

The virtual keyboard to show. Use text for codes with letters.

string"numeric"
textAlign

Where the caret starts when the field is focused by clicking outside a slot.

"left" | "center" | "right""left"
pushPasswordManagerStrategy

Makes room for password-manager badges.

"increase-width" | "none""increase-width"
containerClassName

Classes for the outer container. className styles the hidden input.

stringNo default
disabled

Disables the field and dims every slot.

booleanfalse

InputOTPGroup

Renders a <div> that joins its slots into one bordered segment.

InputOTPSlot

Renders one character cell. Accepts all div props.

PropTypeDefault
indexrequired

The zero-based position of the character this slot displays.

numberNo default

InputOTPSeparator

Renders a <div role="separator"> with a dash between groups.