InputOTP
Accessible one-time password component with copy-paste functionality.
Demo
Dependencies
clsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import {
useRef,
useCallback,
type ClipboardEvent,
type KeyboardEvent,
} from "react";
import { cn } from "@/lib/utils";
export interface InputOTPProps {
value?: string;
onChange?: (value: string) => void;
maxLength?: number;
className?: string;
}
export function InputOTP({
value = "",
onChange,
maxLength = 6,
className,
}: InputOTPProps) {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const focus = useCallback((index: number) => {
inputRefs.current[index]?.focus();
}, []);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>, index: number) => {
if (e.key === "Backspace" && !value[index] && index > 0) {
focus(index - 1);
} else if (e.key === "ArrowLeft" && index > 0) {
focus(index - 1);
} else if (e.key === "ArrowRight" && index < maxLength - 1) {
focus(index + 1);
}
},
[value, maxLength, focus],
);
const handleChange = useCallback(
(val: string, index: number) => {
const digits = value.split("");
digits[index] = val.slice(-1);
const next = digits.join("").slice(0, maxLength);
onChange?.(next);
if (val && index < maxLength - 1) focus(index + 1);
},
[value, maxLength, onChange, focus],
);
const handlePaste = useCallback(
(e: ClipboardEvent) => {
e.preventDefault();
const data = e.clipboardData
.getData("text")
.replace(/\D/g, "")
.slice(0, maxLength);
onChange?.(data);
focus(Math.min(data.length, maxLength - 1));
},
[maxLength, onChange, focus],
);
return (
<div className={cn("flex items-center gap-2", className)}>
{Array.from({ length: maxLength }).map((_, i) => (
<input
key={i}
ref={(el) => {
inputRefs.current[i] = el;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={value[i] ?? ""}
onChange={(e) => handleChange(e.target.value, i)}
onKeyDown={(e) => handleKeyDown(e, i)}
onPaste={handlePaste}
className="flex h-10 w-10 items-center justify-center rounded-md border border-gray-200 text-center text-sm font-medium focus:outline-none focus:ring-2 focus:ring-gray-400"
aria-label={`Digit ${i + 1}`}
/>
))}
</div>
);
}
CLI
npx genui add input-otp