Input OTP
One-time code input with per-character slots. Uses the input-otp library. Slot h-9 w-9, rounded-md, focus ring 3px ring/50, aria-invalid switches to destructive border.
Anatomy
- 1Container
OTP root that manages the hidden input, focus, and paste behavior.
- 2Group
Visual cluster of slots. Slots inside share borders (first is rounded-left, last is rounded-right).
- 3Slot
One character cell: 36×36, border-input, text-sm font-medium. Focus adds ring ring-ring/50; aria-invalid switches to border-destructive.
- 4Separator
Optional visual break between groups (Phosphor Minus, size-4). Purely decorative.
Variants
Four canonical looks. Simple = one group. With separator = 2+2+2 or 3+3 divided by a dash. With spacing = each slot as its own rounded box with a gap. Digits only = uses pattern={REGEXP_ONLY_DIGITS} so keyboards and paste ignore letters.
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP><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>{/* Override each slot to be its own rounded box, add gap on the group */}
<InputOTP maxLength={6}>
<InputOTPGroup className="gap-2">
{[0, 1, 2, 3, 4, 5].map((i) => (
<InputOTPSlot key={i} index={i} className="rounded-md border-l" />
))}
</InputOTPGroup>
</InputOTP>import { REGEXP_ONLY_DIGITS } from "input-otp";
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>States
Default / focus (ring ring-ring/50) / filled / disabled (opacity 50 via has-[:disabled]) / error (aria-invalid on the slot switches border to destructive; focused error swaps the ring to destructive/20).
<InputOTP maxLength={4}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP><InputOTP maxLength={4} defaultValue="1234">…</InputOTP><InputOTP maxLength={4} disabled>…</InputOTP>Invalid code. Try again.
{/* aria-invalid on each slot switches border to destructive */}
<InputOTP maxLength={4} defaultValue="12">
<InputOTPGroup>
{[0, 1, 2, 3].map((i) => (
<InputOTPSlot key={i} index={i} aria-invalid />
))}
</InputOTPGroup>
</InputOTP>
<p className="mt-2 text-xs text-destructive">Invalid code. Try again.</p>Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
6 digits split 3+3 with a dash separator. Pair with a 'Resend code' button below.
<div className="grid gap-3">
<Label>Enter your 6-digit code</Label>
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button variant="link" className="w-fit px-0 text-xs">
Resend code
</Button>
</div>6 digits in a single group, digits-only pattern. Auto-submit on complete via onComplete.
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
onComplete={(value) => verifyCode(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>6 digits shown as individual spaced boxes. Reads as 'code entry field' instead of a joined pill.
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup className="gap-2">
{[0, 1, 2, 3, 4, 5].map((i) => (
<InputOTPSlot key={i} index={i} className="rounded-md border-l" />
))}
</InputOTPGroup>
</InputOTP>Import
Copy this import line at the top of the file where you compose this atom.
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { REGEXP_ONLY_DIGITS } from "input-otp";Props
Everything else from the underlying HTML or Radix primitive is forwarded via ...props.
| Prop | Type | Default | Description |
|---|---|---|---|
| maxLength | number | — | Total number of slots. Should equal the sum of all slot indexes. |
| value / defaultValue | string | — | Controlled or uncontrolled OTP value. |
| onChange | (value: string) => void | — | Fires on every keystroke or paste. |
| onComplete | (value: string) => void | — | Fires once maxLength is reached. Use to auto-submit. |
| pattern | string (regex) | — | Restrict allowed characters. Use REGEXP_ONLY_DIGITS for numeric codes. |
| disabled | boolean | false | Dims the whole group via has-[:disabled]:opacity-50. |
| containerClassName | string | — | Merged onto the OTP root container. |
| InputOTPSlot.index | number | — | Which position this slot binds to. Must be unique per slot. |
| InputOTPSlot.aria-invalid | boolean | — | Switches the slot border to destructive and elevates it above neighbors. Used for error state. |
| InputOTPSlot.className | string | — | Merged onto the slot. Use 'rounded-md border-l' on each slot for the With spacing variant. |
When to use
- Auth flows: 2FA, email verify, phone verify.
- Any short fixed-length code (usually 4-8 chars).
- When the value should be scannable at a glance while typing.
When not to use
- Regular text input — use Input.
- Passwords — use Input type='password'.
- Variable-length codes — use Input.
Usage
- Use a fixed length (usually 6 slots).
- Provide a 'Resend code' link right below the input.
- For numeric codes, pass pattern={REGEXP_ONLY_DIGITS} so mobile keyboards open numeric.
- Trigger onComplete once the last slot is filled — no extra Submit needed.
- Don't split into more than two groups — hurts scannability.
- Don't allow paste of the wrong length silently — clear or truncate.
- Don't add a shadow to slots — the kit keeps them flat.
Related
Cross-links to atoms and patterns you may reach for next.