Build websites 10x faster with HextaUI Blocks — Learn more
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.

components/ui/input-otp.tsx
"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 h-8 w-8 sm:h-10 sm:w-10 items-center justify-center border-y border-r border-input bg-[hsl(var(--hu-input))] text-xs sm:text-sm transition-all first:rounded-l-lg sm:first:rounded-l-xl first:border-l last:rounded-r-lg sm:last:rounded-r-xl focus-within:z-10 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "border-input text-[hsl(var(--hu-foreground))]",
        destructive:
          "border-[hsl(var(--hu-destructive))] text-[hsl(var(--hu-destructive-foreground))] focus-within:ring-[hsl(var(--hu-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-[hsl(var(--hu-ring))] ring-offset-2",
        filled:
          "bg-accent border-[hsl(var(--hu-border))] text-[hsl(var(--hu-accent-foreground))]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      state: "default",
    },
  },
);

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";

    const slotContent = (
      <div
        ref={ref}
        className={cn(
          inputOTPSlotVariants({
            variant,
            size: otpSize,
            state: state || currentState,
          }),
          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-[hsl(var(--hu-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-[hsl(var(--hu-muted-foreground))] px-1 sm:px-2",
      size === "sm"
        ? "text-xs"
        : size === "lg"
          ? "text-sm sm:text-base"
          : "text-xs sm:text-sm",
    )}
    {...props}
  >
    <svg
      width="6"
      height="6"
      viewBox="0 0 8 8"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      className="sm:w-2 sm:h-2"
    >
      <circle cx="4" cy="4" r="1" fill="currentColor" />
    </svg>
  </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

Different Sizes

Small
Default
Large

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-[hsl(var(--hu-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-[hsl(var(--hu-muted-foreground))]">
                Enter your one-time password.
            </div>
            </div>
        </div>
        <div className="flex flex-col gap-4">
            <h3 className="text-sm font-semibold text-[hsl(var(--hu-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-[hsl(var(--hu-foreground))]">
            Different Sizes
            </h3>
            <div className="flex flex-col gap-6">
            <div className="flex flex-col gap-3">
                <div className="text-xs text-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-muted-foreground))]">
                Verifying...
            </div>
            )}

            {isComplete && (
            <div className="text-sm text-[hsl(var(--hu-primary))] font-medium">
                ✓ Verification successful!
            </div>
            )}

            {!isComplete && !isLoading && value.length > 0 && (
            <div className="text-xs text-[hsl(var(--hu-muted-foreground))]">
                {value.length}/6 characters entered
            </div>
            )}
        </div>
        <div className="flex flex-col gap-2">
            <button
            className="text-sm text-[hsl(var(--hu-primary))] hover:underline"
            onClick={() => {
                setValue("");
                setIsComplete(false);
                setIsLoading(false);
            }}
            >
            Resend code
            </button>
            <div className="text-xs text-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-muted-foreground))]">
            Only numbers are allowed
            </div>
        </div>

        <div className="flex flex-col gap-3">
            <h3 className="text-sm font-semibold text-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-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-[hsl(var(--hu-primary))] text-[hsl(var(--hu-primary-foreground))] rounded-md hover:bg-[hsl(var(--hu-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

PropTypeDefault
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

PropTypeDefault
className?
string
undefined
animated?
boolean
true
state?
"default" | "active" | "filled"
"default"
otpSize?
"sm" | "default" | "lg"
"default"
variant?
"default" | "destructive"
"default"
index?
number
undefined

InputOTPGroup

PropTypeDefault
className?
string
undefined
otpSize?
"sm" | "default" | "lg"
"default"
variant?
"default" | "destructive"
"default"

InputOTPSeparator

PropTypeDefault
className?
string
undefined
size?
"sm" | "default" | "lg"
"default"
variant?
"default" | "destructive"
"default"
Edit on GitHub

Last updated on