Input OTP
Accessible one-time password component with copy-paste functionality.
The Input OTP component is built on top of Corvu OTP Field, which provides the accessible one-time password input primitive with copy-paste support.
import { createSignal } from "solid-js";import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
export default function InputOTPDemo() { const [value, setValue] = createSignal("123456");
return ( <InputOTP maxLength={6} value={value()} onValueChange={setValue}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}Installation
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 └── InputOTPSlotThe input only accepts digits by default (Corvu OTP Field's built-in ^\d*$ pattern). Use the pattern prop to customize the accepted characters.
Pattern
Use the pattern prop to define a custom regex for the accepted characters. The default only accepts digits; this example also accepts letters. Pass null to allow all characters.
import { Field, FieldLabel } from "~/components/ui/field";import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
const REGEXP_ONLY_DIGITS_AND_CHARS = "^[a-zA-Z0-9]*$";
export default function InputOTPPattern() { return ( <Field class="w-fit"> <FieldLabel for="digits-and-chars">Digits and characters</FieldLabel> <InputOTP id="digits-and-chars" maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> </Field> );}Alphanumeric
Accept both letters and numbers with an alphanumeric pattern.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "~/components/ui/input-otp";
export default function InputOTPAlphanumeric() { return ( <InputOTP maxLength={6} pattern="^[a-zA-Z0-9]*$"> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> </InputOTPGroup> <InputOTPSeparator /> <InputOTPGroup> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> );}Separator
Use the <InputOTPSeparator /> component to add a separator between input groups.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "~/components/ui/input-otp";
export default 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.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "~/components/ui/input-otp";
export default function InputOTPDisabled() { return ( <InputOTP id="disabled" 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 onValueChange props to control the input value.
import { createSignal, Show } from "solid-js";import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
export default function InputOTPControlled() { const [value, setValue] = createSignal("");
return ( <div class="space-y-2"> <InputOTP maxLength={6} value={value()} onValueChange={setValue}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> <InputOTPSlot index={4} /> <InputOTPSlot index={5} /> </InputOTPGroup> </InputOTP> <div class="text-center text-sm"> <Show when={value() !== ""} fallback="Enter your one-time password."> You entered: {value()} </Show> </div> </div> );}Invalid
Use aria-invalid on the slots to show an error state.
import { createSignal } from "solid-js";import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "~/components/ui/input-otp";
export default function InputOTPInvalid() { const [value, setValue] = createSignal("000000");
return ( <InputOTP maxLength={6} value={value()} onValueChange={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: set maxLength={4}.
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
export default function InputOTPFourDigits() { return ( <InputOTP maxLength={4}> <InputOTPGroup> <InputOTPSlot index={0} /> <InputOTPSlot index={1} /> <InputOTPSlot index={2} /> <InputOTPSlot index={3} /> </InputOTPGroup> </InputOTP> );}Form
/** biome-ignore-all lint/a11y/useValidAnchor: <example file> */import { RefreshCwIcon } from "lucide-solid";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 default function InputOTPForm() { return ( <Card class="mx-auto max-w-md"> <CardHeader> <CardTitle>Verify your login</CardTitle> <CardDescription> Enter the verification code we sent to your email address:{" "} <span class="font-medium">m@example.com</span>. </CardDescription> </CardHeader> <CardContent> <Field> <div class="flex items-center justify-between"> <FieldLabel for="otp-verification">Verification code</FieldLabel> <Button variant="outline" size="xs"> <RefreshCwIcon data-icon="inline-start" /> Resend Code </Button> </div> <InputOTP maxLength={6} id="otp-verification" required> <InputOTPGroup class="*: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 class="mx-2" /> <InputOTPGroup class="*: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" class="w-full"> Verify </Button> <div class="text-muted-foreground text-sm"> Having trouble signing in?{" "} <a href="#" class="underline underline-offset-4 transition-colors hover:text-primary"> Contact support </a> </div> </Field> </CardFooter> </Card> );}API Reference
See the Corvu OTP Field API reference for primitive props, keyboard interactions, and accessibility details.