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> );}pnpm dlx shadcn@latest add @herocn/input-otpInput OTP is built on top of input-otp by @guilherme_rodz.
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>Use the following composition to build an InputOTP:
InputOTP├── InputOTPGroup│ ├── InputOTPSlot│ ├── InputOTPSlot│ └── InputOTPSlot├── InputOTPSeparator├── InputOTPGroup│ ├── InputOTPSlot│ ├── InputOTPSlot│ └── InputOTPSlot├── InputOTPSeparator└── InputOTPGroup ├── InputOTPSlot └── InputOTPSlotUse 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>Use the variant prop for a softer background on the secondary style.
Default
Secondary
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> );}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> );}Use the disabled prop to disable the input.
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> );}Use the value and onChange props to control the input value.
"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> );}Use aria-invalid on the slots to show an error state.
"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> );}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> );}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> );}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.
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> );}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> );}Note that OTP should always have dir="ltr" because users fill OTPs from left to right.
"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> );}| Prop | Type | Default |
|---|---|---|
| variant | "default" | "secondary" | "default" |
See the input-otp documentation for more information.