Input OTP
A one-time password input available in two styles — shadcn's joined pill (stock) and individually boxed slots (this registry's custom take).
Preview
import * as React from "react"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function BasicExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
const [value, setValue] = React.useState("")
return (
<InputOTP
maxLength={6}
value={value}
onChange={setValue}
variant={variant}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
Installation
pnpm dlx shadcn@latest add https://ui-registry-delta.vercel.app/r/input-otp.jsonUsage
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>Pass variant="joined" to opt into shadcn's stock pill styling. Omit it (or pass "boxed") for this registry's default look.
<InputOTP maxLength={6} variant="joined">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>Composition
Use the following composition to build an InputOTP:
InputOTP
├── InputOTPGroup
│ ├── InputOTPSlot
│ ├── InputOTPSlot
│ └── InputOTPSlot
├── InputOTPSeparator
├── InputOTPGroup
│ ├── InputOTPSlot
│ ├── InputOTPSlot
│ └── InputOTPSlot
├── InputOTPSeparator
└── InputOTPGroup
├── InputOTPSlot
└── InputOTPSlotExamples
With separator
Split slots into groups with a visual separator.
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function SeparatorExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
return (
<InputOTP maxLength={6} variant={variant}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
Digits only
Restrict input to numeric characters using the pattern prop.
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
REGEXP_ONLY_DIGITS,
} from "@/components/ui/input-otp"
export default function DigitsOnlyExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
return (
<InputOTP maxLength={4} pattern={REGEXP_ONLY_DIGITS} variant={variant}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
)
}
Disabled
Non-interactive state for read-only or loading contexts.
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function DisabledExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
return (
<InputOTP maxLength={6} disabled variant={variant}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
Controlled
Bind value and onChange to track the OTP in your own state. The live value is displayed below the input.
Enter your one-time password
import * as React from "react"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function ControlledExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
const [value, setValue] = React.useState("")
return (
<div className="flex flex-col items-center gap-4">
<InputOTP
maxLength={6}
value={value}
onChange={setValue}
variant={variant}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<p className="text-center text-sm">
{value === "" ? (
"Enter your one-time password"
) : (
<>
You entered:{" "}
<span className="font-mono font-medium text-foreground">{value}</span>
</>
)}
</p>
</div>
)
}
Invalid
Pass aria-invalid to each InputOTPSlot to show the error styles. Fill all six slots to see the destructive state.
import * as React from "react"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InvalidExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
const [value, setValue] = React.useState("000000")
return (
<InputOTP
maxLength={6}
value={value}
onChange={setValue}
variant={variant}
>
<InputOTPGroup>
<InputOTPSlot index={0} aria-invalid />
<InputOTPSlot index={1} aria-invalid />
<InputOTPSlot index={2} aria-invalid />
<InputOTPSlot index={3} aria-invalid />
<InputOTPSlot index={4} aria-invalid />
<InputOTPSlot index={5} aria-invalid />
</InputOTPGroup>
</InputOTP>
)
}
Alphanumeric
Accept both letters and digits by passing the REGEXP_ONLY_DIGITS_AND_CHARS pattern. Special characters are rejected.
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
REGEXP_ONLY_DIGITS_AND_CHARS,
} from "@/components/ui/input-otp"
export default function AlphanumericExample({
variant = "boxed",
}: {
variant?: "boxed" | "joined"
}) {
return (
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
variant={variant}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
API Reference
InputOTP
Root component. Accepts every prop from the underlying OTPInput in input-otp — the most relevant are listed below.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "boxed" | "joined" | "boxed" | Visual style. "boxed" renders each slot as an individually rounded box (this registry's take); "joined" renders shadcn's stock pill where slots share borders. |
| maxLength | number | — | Total number of characters the input accepts. Required. |
| value | string | — | Controlled value of the input. |
| onChange | (value: string) => void | — | Called whenever the value changes. |
| onComplete | (value: string) => void | — | Called once the user has filled every slot. |
| pattern | string | — | Regex string that restricts allowed characters (e.g. REGEXP_ONLY_DIGITS). |
| disabled | boolean | false | Disables interaction and dims all slots. |
| containerClassName | string | — | Class applied to the slot container, not the hidden input. |
| className | string | — | Class applied to the underlying hidden input element. |
InputOTPSlot
A single character slot. Renders the character, active ring, and blinking caret.
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number | — | Zero-based index of the slot within the group. Must be unique and increasing. |
| className | string | — | Extra classes merged onto the slot wrapper. |
InputOTPGroup & InputOTPSeparator
Layout helpers. InputOTPGroup wraps related slots; InputOTPSeparator renders a dot between groups. Both forward standard div props.
Accessibility
- Full keyboard navigation — typing, backspace, arrow keys, and paste all work as expected.
- Slots expose
data-activefor focus state and honoraria-invalidfor error styling. - The separator is rendered with
role="separator"so it is announced correctly.