Input OTP
A one-time-passcode input rendered as character slots with a fake caret. Outline and subtle variants across four slot sizes.
Playground
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Preview
Compose InputOTP (which owns maxLength and the value) with an InputOTPGroup of indexed InputOTPSlots.
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Installation
npx shadcn@latest add https://espresso-base-ui.vercel.app/r/input-otp.jsonUsage
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"<InputOTP maxLength={4} onComplete={(code) => verify(code)}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>Variants and sizes
variant and size live on the container and cascade to every slot: outline gives each slot a shadow ring, subtle a filled surface, and sizes run xs through lg.
<InputOTP maxLength={4} variant="outline" size="lg">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
<InputOTP maxLength={4} variant="subtle" size="sm">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>Separator
Split the code into groups with InputOTPSeparator, which renders a minus icon between two InputOTPGroups.
<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>Invalid
Set aria-invalid on the InputOTPGroup – typically inside a Field with data-invalid="true" – to tint every slot with the error surface.
<Field data-invalid="true">
<FieldLabel>Verification code</FieldLabel>
<InputOTP maxLength={6}>
<InputOTPGroup aria-invalid>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<FieldError>Invalid code. Please try again.</FieldError>
</Field>API reference
InputOTP forwards every prop of the underlying input-otp OTPInput – including value/onChange for controlled use, onComplete, pattern and disabled – while the variant props style the rendered slots.
InputOTP
| Prop | Type | Default | Description |
|---|---|---|---|
| maxLength | number | – | Required. Number of characters in the code. |
| variant | "outline" | "subtle" | "outline" | Slot surface – shadow ring (outline) or filled secondary background (subtle). |
| size | "xs" | "sm" | "md" | "lg" | "md" | Square slot size (24 / 28 / 32 / 40px) with matching text size and radius; outline trims 2px for its shadow ring. |
| value | string | – | Controlled value; pair with onChange. |
| onChange | (value: string) => void | – | Called on every value change. |
| onComplete | (value: string) => void | – | Called when all maxLength characters are filled. |
| pattern | string | – | Regex the value must match, e.g. "^\\d*$" for digits only. |
| disabled | boolean | false | Disables input and dims the slots. |
| containerClassName | string | – | Class for the outer container (the element that carries the variant styles); className styles the hidden input. |
InputOTPSlot
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number | – | Required. Which character of the value this slot displays. |
| Component | Description |
|---|---|
| InputOTPGroup | Row of adjacent slots (data-slot="input-otp-group"). Set aria-invalid here to switch all slots to the error surface. |
| InputOTPSlot | One character cell with a blinking fake caret when active (data-slot="input-otp-slot", data-active). |
| InputOTPSeparator | Minus-icon divider with role="separator" between groups (data-slot="input-otp-separator"). |
Accessibility & styling hooks
Focus lands on a single real input element (rendered invisibly by input-otp), so typing, arrow keys, backspace and pasting a full code all behave natively while the slots are purely visual – the active slot shows a ring plus an animated fake caret. Label the control with a FieldLabel or aria-label, and reflect errors with aria-invalid on the group.
Styling flows from the container: variant and size classes target [data-slot=input-otp-slot] descendants, the active slot exposes data-active="true", and the parts carry data-slot attributes (input-otp, input-otp-group, input-otp-slot, input-otp-separator) for CSS overrides.