Skip to content

ComponentsForms

Input OTP

A one-time code input split into individual character slots. Backed by a single real input, so paste, SMS and password-manager autofill, and screen readers work. Supports digit or alphanumeric patterns, grouped or separated slots, auto-verify on complete, invalid states and form submission.

input-otpSource

Installation

pnpm dlx shadcn@latest add @desyne/input-otp

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp";
tsx
<InputOTP maxLength={6} aria-label="Verification code">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
  </InputOTPGroup>
  <InputOTPSeparator />
  <InputOTPGroup>
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

InputOTP wraps the input-otp package. It renders one transparent <input> stretched over the slots; the slots only display what's typed, reading each character and the caret position from context. Render one InputOTPSlot per character, with index from 0 to maxLength - 1.

Not a React Aria component

Unlike the other fields, InputOTP takes native input props such as disabled and required, not isDisabled or isRequired. Its onChange receives the new value as a string. The caret needs the animate-caret-blink keyframes, which the registry adds to your CSS on install.

When to use

  • Input OTP — short verification codes of a fixed length: email or SMS codes, authenticator (TOTP) codes, PINs, recovery codes.
  • Text Field — codes of variable length, or when you'd rather show a single box (inputMode="numeric", autoComplete="one-time-code").
  • Number Field — actual quantities. Codes aren't numbers: leading zeros matter.

Anatomy

tsx
<InputOTP>                 {/* container <div> + one real <input> */}
  <InputOTPGroup>          {/* joined or separated set of slots */}
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
  </InputOTPGroup>
  <InputOTPSeparator />    {/* optional visual divider */}
  <InputOTPGroup>…</InputOTPGroup>
</InputOTP>
PartRendersNotes
InputOTP<div> + <input>The container (containerClassName) is a flex row with a gap and dims when the input is disabled. The input (className, data-slot="input-otp") receives every native input prop.
InputOTPGroup<div>variant="joined" (default) draws slots as one segmented box; separated draws individual boxes with a gap.
InputOTPSlot<div>Displays the character at index, a blinking fake caret when it's the active empty slot, and a focus ring while active.
InputOTPSeparator<div role="separator">A muted minus icon between groups. Purely visual.

Examples

Separated slots

variant="separated" on InputOTPGroup renders each slot as its own rounded box. Joined slots read as one field; separated slots suit larger, touch-first layouts.

Digits only

pattern takes a regex source string; characters and pastes that don't match are rejected. The package exports REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS. The mobile keyboard is numeric by default (inputMode="numeric").

Digits only.

Alphanumeric codes

For recovery or invite codes, allow letters with REGEXP_ONLY_DIGITS_AND_CHARS, switch to inputMode="text", and use pasteTransformer to strip spaces or dashes from pasted codes. uppercase on the slots displays letters in capitals; the value keeps the case the user typed.

Paste “K7QD-92XF” — the dash is stripped.

Sizes

Slots are 40px squares by default. Resize them with className on each InputOTPSlot (size-* and a text size), and adjust the group's gap for separated slots.

1
2
1
2
1
2

Placeholder

placeholder on InputOTP is split across the slots: slot n shows character n in a muted color. As in input-otp itself, the placeholder is shown only while the whole input is empty, and it's hidden in the slot that has the blinking caret. The string is also exposed to screen readers as aria-placeholder on the input.

Controlled

value and onChange hold the code as a string. Set it to "" to clear.

0/6 · —

Verify on complete

onComplete fires with the full code when the last slot is filled, so users don't need a submit button. Disable the input while you check it, and announce the result in an aria-live region.

Verifies automatically on the last digit.

Invalid

Slots turn red when they carry aria-invalid. Set it on each InputOTPSlot for the visual state, and on InputOTP (with aria-describedby pointing at the message) so screen readers hear it.

4
8
1
9
2
0

That code is incorrect. Try 123456.

Disabled

disabled disables the input; the container dims through has-disabled:opacity-50.

2
0
4
8

Form submission

The real input submits under its name. Use required and minLength equal to maxLength for native validation, and link a visible <label> with id / htmlFor.

Recipes

Verify email

A verification card: the heading labels the input through aria-labelledby, submit stays disabled until all six digits are in, and a resend link has a cooldown.

Check your email

We sent a 6-digit code to ada@acme.dev.

Didn't get it?

Two-factor sign-in

Authenticator codes with a switch to 10-character recovery codes. Changing mode remounts the input (key) with a new maxLength, pattern and inputMode; a wrong code marks the slots invalid.

Two-factor authentication

Enter the code from your authenticator app.

Accessibility

  • A single native <input> holds the value, so screen readers announce one text field, and paste, SMS one-time-code autofill (autoComplete="one-time-code" is the default) and password managers work.
  • Always provide a label: aria-label, aria-labelledby, or a <label htmlFor> pointing at the input's id. The slots are visual only.
  • The separator has role="separator" and a decorative icon.
  • Announce verification results and errors with aria-live, and set aria-invalid plus aria-describedby on InputOTP for invalid codes; the slot styling alone isn't announced.
  • Prefer onComplete for auto-verify, but keep a way to retry and don't clear the code without telling the user.

Keyboard

KeyAction
TypingFills the active slot and moves to the next one
BackspaceDeletes the previous character
← / →Moves the caret between slots
⌘ / Ctrl + VPastes a whole code, after pasteTransformer and pattern checks

Styling

Data attributes

AttributeElementPresent when
data-active="true"InputOTPSlotThe caret or selection is on this slot while the input is focused
data-placeholderInputOTPSlotThe slot is showing its placeholder character
aria-invalidInputOTPSlotYou pass it; draws a destructive border
data-variantInputOTPGroupAlways: joined or separated
data-input-otp-containerContainerAlways (set by the package)

Slots read their group's variant through group/otp, so group-data-[variant=separated]/otp: targets separated slots from your own classes.

Slots

data-slotElement
input-otpThe real <input>
input-otp-groupGroup
input-otp-slotSlot
input-otp-separatorSeparator

containerClassName styles the row around the slots; className styles the transparent input itself. The fake caret uses the animate-caret-blink utility.

API Reference

InputOTP

Prop

Type

Also accepts every native <input> attribute. See the input-otp docs for the full API.

InputOTPGroup

Prop

Type

Also accepts every <div> prop.

InputOTPSlot

Prop

Type

Also accepts every <div> prop. While the input is empty, the slot renders its character of the input's placeholder (muted, aria-hidden).

InputOTPSeparator

Accepts every <div> prop. Renders role="separator" with a minus icon.