OTP Input
One-time code input with auto-advance, paste and backspace handling.
Check your inbox
Enter the 6-digit code we sent to ada@northwind.io
Didn't get it?
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/otp-input.jsonUsage
import { OTPInput } from "@/components/ui/otp-input"
<OTPInput onComplete={(code) => verify(code)} />Examples
Grouped
groupSize splits the slots with a dash, so long codes are easier to read back.
Masked PIN
mask shows dots instead of digits.
Invalid code
The row shakes and turns red each time invalid switches on.
Letters and numbers
mode="alphanumeric" accepts letters too and brings up the full keyboard on phones.
API reference
Other props and the ref go to the wrapping group element.
| Prop | Type | Default |
|---|---|---|
length | numberNumber of slots. | 6 |
value | stringControlled code. | None |
defaultValue | stringInitial code when uncontrolled. | None |
onValueChange | (code: string) => voidCalled on every change. | None |
onComplete | (code: string) => voidCalled when the last empty slot is filled. | None |
mode | "numeric"Characters accepted. Also picks the mobile keyboard. | "numeric" |
groupSize | numberPuts a dash after every groupSize slots. | None |
mask | booleanShows dots instead of characters. | false |
invalid | booleanRed state. Shakes the row when it switches on. | false |
disabled | booleanDisables every slot. | false |
autoFocus | booleanFocuses the first empty slot on mount. | false |
name | stringSubmits the code with a surrounding form. A form reset empties the slots. | None |
required | booleanStops the submit until every slot is filled, with a message that says how many are needed. | false |
aria-label | stringNames the group of slots. | "One-time code" |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
size | "sm"Slot size and text size. | "md" |
Accessibility
- When the browser rejects the field on submit (required, type, pattern), it turns red until it's edited or the form resets. The browser shows its own message.
- Each slot is a native input labelled "Digit 2 of 6" inside a named group.
- The first slot uses autocomplete="one-time-code", so codes from SMS can fill every slot in one tap.
- Pasting fills from the focused slot. A full-length code always fills from the start, and spaces or dashes are dropped.
- Only the next slot to fill is in the tab order. Arrow keys, Home and End move between slots.
- Backspace clears the focused slot, or steps back and clears the previous one when the focused slot is empty.
- Link an error message with aria-describedby when invalid is on. The shake is skipped when reduced motion is on.
- Slots keep their size while there's room. Give the group w-full min-w-0 in a narrow column and they narrow together instead of overflowing.