Shadcn Input OTP
Free shadcn/ui input OTP for React — a one-time-password field with accessible, copy-paste-friendly slots: a six-slot default, grouped slots with a separator, digit-only input, a controlled value, and a verify card.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
maxLength*
= —
number
Number of slots (e.g. 6 for a 6-digit code).
pattern
= —
string
A regex of allowed chars (e.g. digits only).
value / onChange
= —
string / (v: string) => void
Controlled value of the entered code.
A one-time-password / verification-code input built on the input-otp library (by @guilhermerodz). Set maxLength to the number of digits and compose <InputOTP> with <InputOTPGroup> of <InputOTPSlot index={n} />, optionally split by an <InputOTPSeparator>. Behaves like one input: paste fills it, arrows move between slots. Pass pattern (REGEXP_ONLY_DIGITS) to restrict input. The active slot shows a blinking caret (animate-caret-blink).
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
)
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
)
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
)
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
Examples
Basic
The canonical six-slot one-time-password input.
Packages
Props
No props documented yet.
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPDemo01() {
return (
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
With separator
Two groups of three slots split by a separator.
Packages
Props
No props documented yet.
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPSeparator01() {
return (
<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>
)
}
Digits only
Restricts input to numbers with the REGEXP_ONLY_DIGITS pattern.
Packages
Props
No props documented yet.
"use client"
import { REGEXP_ONLY_DIGITS } from "input-otp"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPPattern01() {
return (
<div className="space-y-2 text-center">
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
{Array.from({ length: 6 }).map((_, i) => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>
<p className="text-muted-foreground text-xs">Digits only</p>
</div>
)
}
Controlled
A controlled OTP that echoes the entered value as you type.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPControlled01() {
const [value, setValue] = React.useState("")
return (
<div className="space-y-3 text-center">
<InputOTP maxLength={6} value={value} onChange={setValue}>
<InputOTPGroup>
{Array.from({ length: 6 }).map((_, i) => (
<InputOTPSlot key={i} index={i} />
))}
</InputOTPGroup>
</InputOTP>
<div className="text-muted-foreground text-sm">
{value === "" ? "Enter your code" : `You entered: ${value}`}
</div>
</div>
)
}
Verify card
An OTP in a verification card with a submit that stays disabled until all slots are filled.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
export default function InputOTPForm01() {
const [value, setValue] = React.useState("")
return (
<Card className="w-full max-w-sm">
<CardHeader className="items-center text-center">
<CardTitle>Verify your email</CardTitle>
<CardDescription>
Enter the 6-digit code we sent to your inbox.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<InputOTP maxLength={6} value={value} onChange={setValue}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</CardContent>
<CardFooter>
<Button className="w-full" disabled={value.length < 6}>
Verify
</Button>
</CardFooter>
</Card>
)
}
The Shadcn Input OTP is a one-time-password field for React — a row of single-character slots that behave like one input. Typing advances, backspace retreats, and pasting a full code fills every slot at once. It is built on the input-otp library, which backs the slots with a single hidden input so paste, autofill, and screen readers all work correctly. The examples below cover a basic six-slot code, grouped slots, digit-only input, a controlled value, and a verify card.
The parts
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
{/* … */}
</InputOTPGroup>
</InputOTP>
InputOTP owns the value and maxLength; InputOTPGroup wraps a run of slots; each InputOTPSlot renders one character box keyed by index. InputOTPSeparator drops a divider between groups.
Paste just works
Because every slot reads from one hidden input, pasting a full code — including an autofilled SMS code — fills all the boxes at once. You don't wire anything up for it.
Digits only
Pass one of the built-in patterns:
import { REGEXP_ONLY_DIGITS } from "input-otp"
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
The library also ships REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS, or pass your own regex string.
Reading the code
Control it and act when it completes:
const [value, setValue] = React.useState("")
<InputOTP
maxLength={6}
value={value}
onChange={setValue}
onComplete={(code) => verify(code)}
>
onComplete fires once every slot is filled — the natural place to auto-submit or enable a verify button (the Verify card example does exactly that).
Grouping
Split a long code into readable chunks with a separator:
<InputOTPGroup>{/* slots 0–2 */}</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>{/* slots 3–5 */}</InputOTPGroup>
Accessibility
The single hidden input means the field is announced as one control, the mobile keyboard shows the right type, and the active slot gets a fake blinking caret (animate-caret-blink) since a real caret can't span separate boxes. Pair it with a <Label> or surrounding copy that says what the code is for.