Use Input OTP for verification codes. Visible slots present one underlying input; do not replace them with separate controlled fields. Verification, expiry, and resend throttling belong in the application.
Live preview
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
export default function InputOTPDemo() {
return (
<InputOTP maxLength={6} aria-label="One-time password">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/input-otp.jsonnpm install input-otp@^1.4.2Create components/ui/input-otp.tsx:
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { cn } from "@/lib/utils";
import { DotIcon } from "lucide-react";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"cn-input-otp flex items-center gap-2 has-disabled:opacity-50",
containerClassName,
)}
spellCheck={false}
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(
"relative flex size-10 items-center justify-center border-y-2 border-r-2 border-input bg-secondary-background text-sm font-base text-foreground transition-all first:rounded-l-base first:border-l-2 last:rounded-r-base data-[active=true]:z-10 data-[active=true]:ring-1 data-[active=true]:ring-ring",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-current duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-separator"
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
role="separator"
{...props}
>
<DotIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
#Usage
"use client";
import { useState } from "react";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
export default function VerificationCode() {
const [code, setCode] = useState("");
return (
<div className="grid gap-2">
<label htmlFor="verification-code">Six-digit verification code</label>
<InputOTP
id="verification-code"
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
value={code}
onChange={setCode}
autoComplete="one-time-code"
inputMode="numeric"
>
<InputOTPGroup>
{Array.from({ length: 6 }, (_, index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<p aria-live="polite">{code.length} of 6 digits entered.</p>
</div>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
InputOTP.maxLength | Accepted character count | Required |
InputOTP.value / onChange | Controlled code string | Uncontrolled |
InputOTP.pattern | Allowed-character regex string | Upstream behavior |
InputOTPGroup | Visual slot group | Flex row |
InputOTPSlot.index | Zero-based slot index | Required |
InputOTPSeparator | Decorative group separator | Dot |
#Accessibility
Label the underlying input and explain the length. Set autocomplete and input mode appropriately and keep paste enabled. Associate verification errors with the field. Do not log or publish real authentication codes, and do not treat a complete-length value as proof that verification succeeded.