Zaidan

Command Palette

Search for a command to run...

GitHub

Input OTP

Accessible one-time password component with copy paste functionality.

Installation

CLI

Manual

Install the following dependencies:

Copy and paste the following code into your project.

import OtpField, { type RootProps as OtpFieldRootProps } from "@corvu/otp-field";
import { Minus } from "lucide-solid";
import { type ComponentProps, Show, splitProps } from "solid-js";
import { cn } from "~/lib/utils";
type InputOTPProps = OtpFieldRootProps &
ComponentProps<"div"> &
Pick<ComponentProps<"input">, "disabled" | "required"> & {
containerClass?: string;
};
const InputOTP = (props: InputOTPProps) => {
const [local, others] = splitProps(props as InputOTPProps, [
"class",
"containerClass",
"children",
"id",
"disabled",
"required",
"value",
"onValueChange",
]);
return (
<OtpField
data-slot="input-otp"
spellcheck={false}
class={cn("z-input-otp flex items-center has-disabled:opacity-50", local.containerClass)}
{...others}
>
<OtpField.Input
id={local.id}
data-slot="input-otp-input"
class={cn("z-input-otp-input disabled:cursor-not-allowed", local.class)}
spellcheck={false}
disabled={local.disabled}
required={local.required}
value={local.value}
onChange={(e) => local.onValueChange?.(e.target.value)}
/>
{local.children}
</OtpField>
);
};
type InputOTPGroupProps = ComponentProps<"div">;
const InputOTPGroup = (props: InputOTPGroupProps) => {
const [local, others] = splitProps(props, ["class"]);
return (
<div
data-slot="input-otp-group"
class={cn("z-input-otp-group flex items-center", local.class)}
{...others}
/>
);
};
type InputOTPSlotProps = ComponentProps<"div"> & {
index: number;
};
const InputOTPSlot = (props: InputOTPSlotProps) => {
const [local, others] = splitProps(props, ["index", "class"]);
const context = OtpField.useContext();
const char = () => context.value()[local.index];
const isActive = () => context.activeSlots().includes(local.index);
const showCaret = () => isActive() && context.isInserting();
return (
<div
data-slot="input-otp-slot"
data-active={isActive()}
class={cn(
"relative z-input-otp-slot flex items-center justify-center data-[active=true]:z-10",
local.class,
)}
{...others}
>
{char()}
<Show when={showCaret()}>
<div class="pointer-events-none absolute inset-0 z-input-otp-caret flex items-center justify-center">
<div class="z-input-otp-caret-line h-4 w-px animate-caret-blink bg-foreground" />
</div>
</Show>
</div>
);
};
type InputOTPSeparatorProps = ComponentProps<"div">;
const InputOTPSeparator = (props: InputOTPSeparatorProps) => {
const [local, others] = splitProps(props, ["class"]);
return (
<div
data-slot="input-otp-separator"
class={cn("z-input-otp-separator flex items-center", local.class)}
aria-hidden="true"
{...others}
>
<Minus />
</div>
);
};
export {
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
type InputOTPProps,
type InputOTPGroupProps,
type InputOTPSlotProps,
type InputOTPSeparatorProps,
};

Examples

Here are the source code of all the examples from the preview page:

Form

import { RefreshCcw } from "lucide-solid";
import { Example } from "~/components/example";
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";
function InputOTPForm() {
return (
<Example title="Form">
<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>
<form>
<Field>
<div class="flex items-center justify-between">
<FieldLabel for="otp-verification">Verification code</FieldLabel>
<Button variant="outline" size="xs">
<RefreshCcw data-icon="inline-start" />
Resend Code
</Button>
</div>
<InputOTP maxLength={6} id="otp-verification" required>
<InputOTPGroup class="style-lyra:*:data-[slot=input-otp-slot]:h-12 style-maia:*:data-[slot=input-otp-slot]:h-16 style-mira:*:data-[slot=input-otp-slot]:h-12 style-nova:*:data-[slot=input-otp-slot]:h-12 style-vega:*:data-[slot=input-otp-slot]:h-16 style-lyra:*:data-[slot=input-otp-slot]:w-11 style-maia:*:data-[slot=input-otp-slot]:w-12 style-mira:*:data-[slot=input-otp-slot]:w-11 style-nova:*:data-[slot=input-otp-slot]:w-11 style-vega:*:data-[slot=input-otp-slot]:w-12 *:data-[slot=input-otp-slot]:text-xl">
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup class="style-lyra:*:data-[slot=input-otp-slot]:h-12 style-maia:*:data-[slot=input-otp-slot]:h-16 style-mira:*:data-[slot=input-otp-slot]:h-12 style-nova:*:data-[slot=input-otp-slot]:h-12 style-vega:*:data-[slot=input-otp-slot]:h-16 style-lyra:*:data-[slot=input-otp-slot]:w-11 style-maia:*:data-[slot=input-otp-slot]:w-12 style-mira:*:data-[slot=input-otp-slot]:w-11 style-nova:*:data-[slot=input-otp-slot]:w-11 style-vega:*:data-[slot=input-otp-slot]:w-12 *: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>
</form>
</CardContent>
<CardFooter class="flex-col gap-2">
<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>
</CardFooter>
</Card>
</Example>
);
}

Simple

import { Example } from "~/components/example";
import { Field, FieldLabel } from "~/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "~/components/ui/input-otp";
function InputOTPSimple() {
return (
<Example title="Simple">
<Field>
<FieldLabel for="simple">Simple</FieldLabel>
<InputOTP id="simple" 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>
</Field>
</Example>
);
}

Digits Only

import { Example } from "~/components/example";
import { Field, FieldLabel } from "~/components/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
function InputOTPPattern() {
return (
<Example title="Digits Only">
<Field>
<FieldLabel for="digits-only">Digits Only</FieldLabel>
<InputOTP id="digits-only" maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</Field>
</Example>
);
}

With Separator

import { createSignal } from "solid-js";
import { Example } from "~/components/example";
import { Field, FieldLabel } from "~/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "~/components/ui/input-otp";
function InputOTPWithSeparator() {
const [value, setValue] = createSignal("123456");
return (
<Example title="With Separator">
<Field>
<FieldLabel for="with-separator">With Separator</FieldLabel>
<InputOTP id="with-separator" maxLength={6} value={value()} onValueChange={setValue}>
<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>
</Field>
</Example>
);
}

Alphanumeric

import { Example } from "~/components/example";
import { Field, FieldDescription, FieldLabel } from "~/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "~/components/ui/input-otp";
function InputOTPAlphanumeric() {
return (
<Example title="Alphanumeric">
<Field>
<FieldLabel for="alphanumeric">Alphanumeric</FieldLabel>
<FieldDescription>Accepts both letters and numbers.</FieldDescription>
<InputOTP id="alphanumeric" 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>
</Field>
</Example>
);
}

Disabled

import { Example } from "~/components/example";
import { Field, FieldLabel } from "~/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "~/components/ui/input-otp";
function InputOTPDisabled() {
return (
<Example title="Disabled">
<Field>
<FieldLabel for="disabled">Disabled</FieldLabel>
<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>
</Field>
</Example>
);
}

4 Digits

import { Example } from "~/components/example";
import { Field, FieldDescription, FieldLabel } from "~/components/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/ui/input-otp";
function InputOTPFourDigits() {
return (
<Example title="4 Digits">
<Field>
<FieldLabel for="four-digits">4 Digits</FieldLabel>
<FieldDescription>Common pattern for PIN codes.</FieldDescription>
<InputOTP id="four-digits" maxLength={4}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
</Field>
</Example>
);
}

Invalid State

import { createSignal } from "solid-js";
import { Example } from "~/components/example";
import { Field, FieldDescription, FieldError, FieldLabel } from "~/components/ui/field";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "~/components/ui/input-otp";
function InputOTPInvalid() {
const [value, setValue] = createSignal("000000");
return (
<Example title="Invalid State">
<Field>
<FieldLabel for="invalid">Invalid State</FieldLabel>
<FieldDescription>Example showing the invalid error state.</FieldDescription>
<InputOTP id="invalid" 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>
<FieldError errors={[{ message: "Invalid code. Please try again." }]} />
</Field>
</Example>
);
}