UI/UI
Input OTP
A flexible and accessible one-time password input component with customizable slots, patterns, and animations.
-
Enter your one-time password.
<div className="flex flex-col gap-8 max-w-md mx-auto">
<div className="flex flex-col gap-3 text-center">
<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>
<div className="text-sm text-muted-foreground">
Enter your one-time password.
</div>
</div>
</div>
Installation
Install following dependencies:
npm install input-otp class-variance-authority motion
pnpm add input-otp class-variance-authority motion
yarn add input-otp class-variance-authority motion
bun add input-otp class-variance-authority motion
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
const inputOTPVariants = cva(
"flex items-center gap-1 sm:gap-2 has-[:disabled]:opacity-50",
{
variants: {
variant: {
default: "",
destructive: "",
},
size: {
sm: "gap-0.5 sm:gap-1",
default: "gap-1 sm:gap-2",
lg: "gap-2 sm:gap-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
const inputOTPSlotVariants = cva(
"relative flex items-center justify-center border-y border-r border-border bg-input text-xs sm:text-sm transition-all focus-within:z-10 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 border-l shadow-sm/2",
{
variants: {
variant: {
default: "border-border text-foreground",
destructive:
"border-destructive text-destructive-foreground focus-within:ring-ring",
},
size: {
sm: "h-6 w-6 sm:h-8 sm:w-8 text-xs",
default: "h-8 w-8 sm:h-10 sm:w-10 text-xs sm:text-sm",
lg: "h-10 w-10 sm:h-12 sm:w-12 text-sm sm:text-base",
},
state: {
default: "",
active:
"border-primary ring-2 ring-ring ring-offset-2",
filled:
"bg-accent border-border text-accent-foreground",
},
position: {
first: "border-l rounded-l-ele",
middle: "rounded-sm",
last: "rounded-r-ele",
single: "border-l rounded-ele",
},
},
defaultVariants: {
variant: "default",
size: "default",
state: "default",
position: "middle",
},
}
);
export interface InputOTPProps {
maxLength: number;
value?: string;
onChange?: (newValue: string) => void;
onComplete?: (newValue: string) => void;
disabled?: boolean;
pattern?: string;
className?: string;
containerClassName?: string;
animated?: boolean;
variant?: "default" | "destructive";
otpSize?: "sm" | "default" | "lg";
children?: React.ReactNode;
}
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
InputOTPProps
>(
(
{
className,
containerClassName,
variant,
otpSize,
animated = true,
children,
...props
},
ref
) => (
<OTPInput
ref={ref}
containerClassName={cn(
inputOTPVariants({ variant, size: otpSize }),
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
>
{children}
</OTPInput>
)
);
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> &
Omit<VariantProps<typeof inputOTPVariants>, "size"> & {
otpSize?: "sm" | "default" | "lg";
}
>(({ className, variant, otpSize, ...props }, ref) => (
<div
ref={ref}
className={cn(inputOTPVariants({ variant, size: otpSize }), className)}
{...props}
/>
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> &
Omit<VariantProps<typeof inputOTPSlotVariants>, "size"> & {
index: number;
animated?: boolean;
otpSize?: "sm" | "default" | "lg";
}
>(
(
{ index, className, variant, otpSize, state, animated = true, ...props },
ref
) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
const currentState = isActive ? "active" : char ? "filled" : "default";
// Determine position based on index and total slots
const totalSlots = inputOTPContext.slots.length;
const position =
totalSlots === 1
? "single"
: index === 0
? "first"
: index === totalSlots - 1
? "last"
: "middle";
const slotContent = (
<div
ref={ref}
className={cn(
inputOTPSlotVariants({
variant,
size: otpSize,
state: state || currentState,
position,
}),
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<motion.div
className="h-3 w-px sm:h-4 sm:w-px bg-foreground"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 0] }}
transition={{
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</div>
)}
</div>
);
if (!animated) return slotContent;
return (
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{
duration: 0.2,
delay: index * 0.05,
ease: "easeOut",
}}
>
{slotContent}
</motion.div>
);
}
);
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & VariantProps<typeof inputOTPVariants>
>(({ variant, size, ...props }, ref) => (
<div
ref={ref}
role="separator"
className={cn(
"flex items-center justify-center text-muted-foreground",
size === "sm"
? "text-xs"
: size === "lg"
? "text-sm sm:text-base"
: "text-xs sm:text-sm"
)}
{...props}
>
-
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
inputOTPVariants,
inputOTPSlotVariants,
};
npx hextaui@latest add input-otp
pnpm dlx hextaui@latest add input-otp
yarn dlx hextaui@latest add input-otp
bun x hextaui@latest add input-otp
Usage
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
} 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>
Examples
Basic Examples
Basic OTP Input
Enter your one-time password.
With Separator
-
Variants
Default
Destructive
Invalid verification code. Please try again.
import { useState } from "react";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
} from "@/components/ui/input-otp";
function InputOTPExamples() {
const [value, setValue] = useState("");
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Basic OTP Input
</h3>
<div className="flex flex-col items-center gap-3">
<InputOTP
maxLength={6}
value={value}
onChange={(value) => setValue(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-center text-sm text-muted-foreground">
Enter your one-time password.
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
With Separator
</h3>
<div className="flex justify-center">
<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>
</div>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Different Sizes
</h3>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<div className="text-xs text-muted-foreground text-center">
Small
</div>
<div className="flex justify-center">
<InputOTP maxLength={4} otpSize="sm">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="text-xs text-muted-foreground text-center">
Default
</div>
<div className="flex justify-center">
<InputOTP maxLength={4} otpSize="default">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="text-xs text-muted-foreground text-center">
Large
</div>
<div className="flex justify-center">
<InputOTP maxLength={4} otpSize="lg">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-foreground">
Variants
</h3>
<div className="flex flex-col gap-4 items-center text-center">
<div className="flex flex-col gap-2">
<div className="text-xs text-muted-foreground">
Default
</div>
<InputOTP maxLength={4} variant="default">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
</div>
<div className="flex flex-col gap-2 items-center text-center">
<div className="text-xs text-muted-foreground">
Destructive
</div>
<InputOTP maxLength={4} variant="destructive">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
<div className="text-xs text-destructive">
Invalid verification code. Please try again.
</div>
</div>
</div>
</div>
</div>
);
}
Complete Verification Flow
Verify your email
We've sent a verification code to your email address.
-
Didn't receive the code? Check your spam folder.
import { useState } from "react";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
} from "@/components/ui/input-otp";
function InputOTPCompleteExample() {
const [value, setValue] = useState("");
const [isComplete, setIsComplete] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleComplete = async (value: string) => {
setIsLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsComplete(true);
setIsLoading(false);
};
const handleChange = (newValue: string) => {
setValue(newValue);
setIsComplete(false);
if (newValue.length === 6) {
handleComplete(newValue);
}
};
return (
<div className="flex flex-col gap-6 max-w-md mx-auto text-center items-center">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">Verify your email</h3>
<p className="text-sm text-muted-foreground">
We've sent a verification code to your email address.
</p>
</div>
<div className="flex flex-col gap-4">
<InputOTP
maxLength={6}
value={value}
onChange={handleChange}
disabled={isLoading || isComplete}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
{isLoading && (
<div className="text-sm text-muted-foreground">
Verifying...
</div>
)}
{isComplete && (
<div className="text-sm text-primary font-medium">
✓ Verification successful!
</div>
)}
{!isComplete && !isLoading && value.length > 0 && (
<div className="text-xs text-muted-foreground">
{value.length}/6 characters entered
</div>
)}
</div>
<div className="flex flex-col gap-2">
<button
className="text-sm text-primary hover:underline"
onClick={() => {
setValue("");
setIsComplete(false);
setIsLoading(false);
}}
>
Resend code
</button>
<div className="text-xs text-muted-foreground">
Didn't receive the code? Check your spam folder.
</div>
</div>
</div>
);
}
Pattern Validation
Numeric Only
-
Only numbers are allowed
Alphanumeric
-
Letters and numbers are allowed
import { useState } from "react";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
function InputOTPPatternExample() {
const [value, setValue] = useState("");
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-semibold text-foreground">
Numeric Only
</h3>
<InputOTP
maxLength={6}
pattern={"[0-9]*"}
value={value}
onChange={setValue}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSeparator />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-xs text-muted-foreground">
Only numbers are allowed
</div>
</div>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-semibold text-foreground">
Alphanumeric
</h3>
<InputOTP maxLength={6} pattern={"[A-Za-z0-9]*"}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSeparator />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-xs text-muted-foreground">
Letters and numbers are allowed
</div>
</div>
</div>
);
}
Animated Slots
Enter your backup codes
Enter any of your 8-digit backup codes
Code 1
Code 2
Code 3
Responsive Design
Automatically adjusts size for mobile devices
-
Try resizing your browser to see the responsive behavior
import { useState } from "react";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
function InputOTPAnimatedExample() {
const [values, setValues] = useState<string[]>(["", "", ""]);
const [currentInput, setCurrentInput] = useState(0);
const handleValueChange = (index: number, value: string) => {
const newValues = [...values];
newValues[index] = value;
setValues(newValues);
if (value.length === 4 && index < 2) {
setCurrentInput(index + 1);
}
};
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2 text-center">
<h3 className="text-lg font-semibold">Enter your backup codes</h3>
<p className="text-sm text-muted-foreground">
Enter any of your 8-digit backup codes
</p>
</div>
<div className="flex flex-col gap-4 items-center justify-center text-center">
{values.map((value, index) => (
<div key={index} className="flex flex-col gap-2">
<div className="text-xs font-medium text-muted-foreground">
Code {index + 1}
</div>
<InputOTP
maxLength={4}
value={value}
onChange={(newValue) => handleValueChange(index, newValue)}
pattern={"[0-9]*"}
animated={true}
>
<InputOTPGroup>
<InputOTPSlot index={0} animated={true} />
<InputOTPSlot index={1} animated={true} />
<InputOTPSlot index={2} animated={true} />
<InputOTPSlot index={3} animated={true} />
</InputOTPGroup>
</InputOTP>
</div>
))}
</div>
<div className="text-center">
<button
className="px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/80 transition-colors disabled:opacity-50"
disabled={!values.some((v) => v.length === 4)}
>
Verify backup code
</button>
</div>
</div>
);
}
Disabled State
Disabled Input
-
This field is currently disabled
<div className="flex flex-col gap-4 max-w-md mx-auto">
<div className="flex flex-col gap-2">
<div className="text-sm font-medium">Disabled Input</div>
<InputOTP maxLength={6} disabled>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-xs text-muted-foreground">
This field is currently disabled
</div>
</div>
</div>
Error State
Verification Code
-
Invalid verification code. Please try again.
<div className="flex flex-col gap-4 max-w-md mx-auto">
<div className="flex flex-col gap-2">
<div className="text-sm font-medium">Verification Code</div>
<InputOTP maxLength={6} variant="destructive">
<InputOTPGroup>
<InputOTPSlot index={0} variant="destructive" />
<InputOTPSlot index={1} variant="destructive" />
<InputOTPSlot index={2} variant="destructive" />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} variant="destructive" />
<InputOTPSlot index={4} variant="destructive" />
<InputOTPSlot index={5} variant="destructive" />
</InputOTPGroup>
</InputOTP>
<div className="text-xs text-destructive">
Invalid verification code. Please try again.
</div>
</div>
</div>
Responsive Design
The InputOTP component automatically adapts to different screen sizes, providing optimal usability on both desktop and mobile devices.
Responsive Layout
-
Automatically adjusts size for mobile devices
<div className="flex flex-col gap-4 max-w-md mx-auto">
<div className="flex flex-col gap-3">
<h3 className="text-sm font-semibold">Responsive Layout</h3>
<div className="flex justify-center">
<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>
</div>
<div className="text-xs text-muted-foreground text-center">
Automatically adjusts size for mobile devices
</div>
</div>
</div>
Features:
- Mobile-first design: Smaller slots (8×8) on mobile, larger (10×10) on desktop
- Responsive gaps: Reduced spacing between slots on mobile devices
- Touch-friendly: Appropriate touch targets for mobile interaction
- Optimized typography: Smaller text on mobile, standard size on desktop
- Adaptive separators: Responsive sizing for visual separators
Props
InputOTP
Prop | Type | Default |
---|---|---|
className? | string | undefined |
containerClassName? | string | undefined |
animated? | boolean | true |
otpSize? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "destructive" | "default" |
disabled? | boolean | false |
pattern? | string | undefined |
onComplete? | (value: string) => void | undefined |
onChange? | (value: string) => void | undefined |
value? | string | undefined |
maxLength? | number | 6 |
InputOTPSlot
Prop | Type | Default |
---|---|---|
className? | string | undefined |
animated? | boolean | true |
state? | "default" | "active" | "filled" | "default" |
otpSize? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "destructive" | "default" |
index? | number | undefined |
InputOTPGroup
Prop | Type | Default |
---|---|---|
className? | string | undefined |
otpSize? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "destructive" | "default" |
InputOTPSeparator
Prop | Type | Default |
---|---|---|
className? | string | undefined |
size? | "sm" | "default" | "lg" | "default" |
variant? | "default" | "destructive" | "default" |
Edit on GitHub
Last updated on