Command Palette

Search for a command to run...

Github

Input OTP

Accessible one-time password component with copy-paste functionality.

1
2
3
4
5
6
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPDemo() {
	return (
		<InputOTP maxLength={6} defaultValue="123456">
			<InputOTPGroup>
				<InputOTPSlot index={0} />
				<InputOTPSlot index={1} />
				<InputOTPSlot index={2} />
				<InputOTPSlot index={3} />
				<InputOTPSlot index={4} />
				<InputOTPSlot index={5} />
			</InputOTPGroup>
		</InputOTP>
	);
}

Installation

pnpm dlx shadcn@latest add https://herocn.dev/r/input-otp.json

About

Input OTP is built on top of input-otp by @guilherme_rodz.

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} 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>

Composition

Use the following composition to build an InputOTP:

InputOTP
├── InputOTPGroup
│   ├── InputOTPSlot
│   ├── InputOTPSlot
│   └── InputOTPSlot
├── InputOTPSeparator
├── InputOTPGroup
│   ├── InputOTPSlot
│   ├── InputOTPSlot
│   └── InputOTPSlot
├── InputOTPSeparator
└── InputOTPGroup
    ├── InputOTPSlot
    └── InputOTPSlot

Pattern

Use the pattern prop to define a custom pattern for the OTP input.

import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"

<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
  ...
</InputOTP>

Examples

Variants

Use the variant prop for a softer background on the secondary style.

Default

1
2
3
4
5
6

Secondary

1
2
3
4
5
6
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPVariants() {
	return (
		<div className="flex w-full max-w-sm flex-col gap-6">
			<div className="space-y-2">
				<p className="text-muted-foreground text-sm">Default</p>
				<InputOTP maxLength={6} defaultValue="123456">
					<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="space-y-2">
				<p className="text-muted-foreground text-sm">Secondary</p>
				<InputOTP maxLength={6} defaultValue="123456" variant="secondary">
					<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>
	);
}

Separator

Use the <InputOTPSeparator /> component to add a separator between input groups.

import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPWithSeparator() {
	return (
		<InputOTP maxLength={6}>
			<InputOTPGroup>
				<InputOTPSlot index={0} />
				<InputOTPSlot index={1} />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={2} />
				<InputOTPSlot index={3} />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={4} />
				<InputOTPSlot index={5} />
			</InputOTPGroup>
		</InputOTP>
	);
}

Disabled

Use the disabled prop to disable the input.

1
2
3
4
5
6
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPDisabled() {
	return (
		<InputOTP maxLength={6} disabled value="123456">
			<InputOTPGroup>
				<InputOTPSlot index={0} />
				<InputOTPSlot index={1} />
				<InputOTPSlot index={2} />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={3} />
				<InputOTPSlot index={4} />
				<InputOTPSlot index={5} />
			</InputOTPGroup>
		</InputOTP>
	);
}

Controlled

Use the value and onChange props to control the input value.

Enter your one-time password.
"use client";

import * as React from "react";

import {
	InputOTP,
	InputOTPGroup,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPControlled() {
	const [value, setValue] = React.useState("");

	return (
		<div className="space-y-2">
			<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">
				{value === "" ? (
					<>Enter your one-time password.</>
				) : (
					<>You entered: {value}</>
				)}
			</div>
		</div>
	);
}

Invalid

Use aria-invalid on the slots to show an error state.

0
0
0
0
0
0
"use client";

import * as React from "react";

import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPInvalid() {
	const [value, setValue] = React.useState("000000");

	return (
		<InputOTP maxLength={6} value={value} onChange={setValue}>
			<InputOTPGroup>
				<InputOTPSlot index={0} aria-invalid />
				<InputOTPSlot index={1} aria-invalid />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={2} aria-invalid />
				<InputOTPSlot index={3} aria-invalid />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={4} aria-invalid />
				<InputOTPSlot index={5} aria-invalid />
			</InputOTPGroup>
		</InputOTP>
	);
}

Four Digits

A common pattern for PIN codes. This uses the pattern={REGEXP_ONLY_DIGITS} prop.

"use client";

import { REGEXP_ONLY_DIGITS } from "input-otp";

import {
	InputOTP,
	InputOTPGroup,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPFourDigits() {
	return (
		<InputOTP maxLength={4} pattern={REGEXP_ONLY_DIGITS}>
			<InputOTPGroup>
				<InputOTPSlot index={0} />
				<InputOTPSlot index={1} />
				<InputOTPSlot index={2} />
				<InputOTPSlot index={3} />
			</InputOTPGroup>
		</InputOTP>
	);
}

Alphanumeric

Use REGEXP_ONLY_DIGITS_AND_CHARS to accept both letters and numbers.

"use client";

import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";

import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPAlphanumeric() {
	return (
		<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
			<InputOTPGroup>
				<InputOTPSlot index={0} />
				<InputOTPSlot index={1} />
				<InputOTPSlot index={2} />
			</InputOTPGroup>
			<InputOTPSeparator />
			<InputOTPGroup>
				<InputOTPSlot index={3} />
				<InputOTPSlot index={4} />
				<InputOTPSlot index={5} />
			</InputOTPGroup>
		</InputOTP>
	);
}

In Surface

Use variant="secondary" when placing an InputOTP inside a card or panel surface for a subtler, shadow-free appearance that blends with the surface background.

1
2
3
4
5
6
import { Field, FieldLabel } from "@/components/ui/field";
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSlot,
} from "@/components/ui/input-otp";
import { Surface } from "@/components/ui/surface";

export function InputOTPInSurface() {
	return (
		<Surface className="flex w-full max-w-sm flex-col gap-4 rounded-3xl p-6">
			<Field>
				<FieldLabel htmlFor="input-otp-surface">Verification Code</FieldLabel>
				<InputOTP
					id="input-otp-surface"
					maxLength={6}
					defaultValue="123456"
					variant="secondary"
				>
					<InputOTPGroup>
						<InputOTPSlot index={0} />
						<InputOTPSlot index={1} />
						<InputOTPSlot index={2} />
						<InputOTPSlot index={3} />
						<InputOTPSlot index={4} />
						<InputOTPSlot index={5} />
					</InputOTPGroup>
				</InputOTP>
			</Field>
		</Surface>
	);
}

Form

Verify your login
Enter the verification code we sent to your email address: [email protected].

I no longer have access to this email address.

Having trouble signing in? Contact support
import { RefreshCwIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
	Card,
	CardContent,
	CardDescription,
	CardFooter,
	CardHeader,
	CardTitle,
} from "@/components/ui/card";
import {
	Field,
	FieldDescription,
	FieldLabel,
} from "@/components/ui/field";
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSeparator,
	InputOTPSlot,
} from "@/components/ui/input-otp";

export function InputOTPForm() {
	return (
		<Card className="mx-auto max-w-md">
			<CardHeader>
				<CardTitle>Verify your login</CardTitle>
				<CardDescription>
					Enter the verification code we sent to your email address:{" "}
					<span className="font-medium">[email protected]</span>.
				</CardDescription>
			</CardHeader>
			<CardContent>
				<Field>
					<div className="flex items-center justify-between">
						<FieldLabel htmlFor="otp-verification">
							Verification code
						</FieldLabel>
						<Button variant="outline" size="xs">
							<RefreshCwIcon />
							Resend Code
						</Button>
					</div>
					<InputOTP
						variant="secondary"
						maxLength={6}
						id="otp-verification"
						required
					>
						<InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
							<InputOTPSlot index={0} />
							<InputOTPSlot index={1} />
							<InputOTPSlot index={2} />
						</InputOTPGroup>
						<InputOTPSeparator className="mx-2" />
						<InputOTPGroup className="*:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-11 *:data-[slot=input-otp-slot]:text-xl">
							<InputOTPSlot index={3} />
							<InputOTPSlot index={4} />
							<InputOTPSlot index={5} />
						</InputOTPGroup>
					</InputOTP>
					<FieldDescription>
						<a href="#">I no longer have access to this email address.</a>
					</FieldDescription>
				</Field>
			</CardContent>
			<CardFooter>
				<Field>
					<Button type="submit" className="w-full">
						Verify
					</Button>
					<div className="text-muted-foreground text-sm">
						Having trouble signing in?{" "}
						<a
							href="#"
							className="underline underline-offset-4 transition-colors hover:text-primary"
						>
							Contact support
						</a>
					</div>
				</Field>
			</CardFooter>
		</Card>
	);
}

RTL

Note that OTP should always have dir="ltr" because users fill OTPs from left to right.

1
2
3
4
5
6
"use client";

import {
	type Translations,
	useTranslation,
} from "@/components/language-selector";
import { Field, FieldLabel } from "@/components/ui/field";
import {
	InputOTP,
	InputOTPGroup,
	InputOTPSlot,
} from "@/components/ui/input-otp";

const translations: Translations = {
	en: {
		dir: "ltr",
		values: {
			verificationCode: "Verification code",
		},
	},
	ar: {
		dir: "rtl",
		values: {
			verificationCode: "رمز التحقق",
		},
	},
	he: {
		dir: "rtl",
		values: {
			verificationCode: "קוד אימות",
		},
	},
};

export function InputOTPRtl() {
	const { dir, language, t } = useTranslation(translations, "ar");

	return (
		<div lang={language} dir={dir}>
			<Field className="mx-auto max-w-xs">
				<FieldLabel htmlFor="input-otp-rtl">{t.verificationCode}</FieldLabel>
				<div dir="ltr">
					<InputOTP maxLength={6} defaultValue="123456" id="input-otp-rtl">
						<InputOTPGroup>
							<InputOTPSlot index={0} />
							<InputOTPSlot index={1} />
							<InputOTPSlot index={2} />
							<InputOTPSlot index={3} />
							<InputOTPSlot index={4} />
							<InputOTPSlot index={5} />
						</InputOTPGroup>
					</InputOTP>
				</div>
			</Field>
		</div>
	);
}

API Reference

InputOTP

PropTypeDefault
variant"default" | "secondary"
"default"

See the input-otp documentation for more information.