Input OTP
One-time password input with slot groups and separator. Built on input-otp.
Examples
Digits only
Pass a pattern (REGEXP_ONLY_DIGITS) to reject non-matching keystrokes.
import { Field, FieldLabel } from "@codefast/ui/field";
import { REGEXP_ONLY_DIGITS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";
export function InputOTPPattern() {
return (
<Field className="w-fit">
<FieldLabel htmlFor="digits-only">Digits Only</FieldLabel>
<InputOTP id="digits-only" maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</Field>
);
}
Disabled
A non-interactive code input.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";
export 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>
);
}
Alphanumeric
Use REGEXP_ONLY_DIGITS_AND_CHARS to accept both letters and numbers.
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/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>
);
}
Controlled
Use the value and onChange props to control the input value.
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";
import * as React from "react";
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>
);
}
Form
Accessible one-time password component with copy-paste functionality.
import { Button } from "@codefast/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@codefast/ui/card";
import { Field, FieldDescription, FieldLabel } from "@codefast/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";
import { RefreshCwIcon } from "lucide-react";
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">m@example.com</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 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-sm text-muted-foreground">
Having trouble signing in?{" "}
<a href="/" className="underline underline-offset-4 transition-colors hover:text-primary">
Contact support
</a>
</div>
</Field>
</CardFooter>
</Card>
);
}
Four Digits
A common pattern for PIN codes. This uses the pattern={REGEXP_ONLY_DIGITS} prop.
import { REGEXP_ONLY_DIGITS } from "@codefast/ui/input-otp";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/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>
);
}
Invalid
Use aria-invalid on the slots to show an error state.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/ui/input-otp";
import * as React from "react";
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>
);
}
RTL
Right-to-left layout support for languages such as Arabic and Hebrew.
Translations are AI-generated for demonstration and may be imperfect.
import { Field, FieldLabel } from "@codefast/ui/field";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@codefast/ui/input-otp";
import { useState } from "react";
import type { Translations } from "#/features/components-catalog/components/detail/language";
import { useTranslation } from "#/features/components-catalog/components/detail/language-context";
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, t } = useTranslation(translations, "ar");
const [value, setValue] = useState("123456");
return (
<Field className="mx-auto max-w-xs">
<FieldLabel htmlFor="input-otp-rtl">{t.verificationCode}</FieldLabel>
<InputOTP dir={dir} id="input-otp-rtl" maxLength={6} onChange={setValue} value={value}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</Field>
);
}
Separator
Use the <InputOTPSeparator /> component to add a separator between input groups.
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@codefast/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>
);
}
Anatomy
How the parts nest — every slot the component exposes, in composition order.
Features
- Renders a single hidden <input> under the hood — pasting a full code fills every slot at once, no per-slot paste handling needed.
- Ships REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS, and REGEXP_ONLY_DIGITS_AND_CHARS (re-exported from input-otp) for the pattern prop.
- onComplete fires once every slot is filled, separate from the per-keystroke onChange.
- InputOTPSeparator drops a visual divider between groups (e.g. 3 + 3 for a 6-digit code) without affecting the value.
API reference
Props for each part of the component. All native element props are also forwarded.
InputOTP
One-time-code input built on input-otp.
maxLengthnumberTotal number of slots (required).
valuestringThe controlled value — the concatenated characters.
onChange(value: string) => voidCalled with the concatenated characters when the value changes.
onComplete(value: string) => voidFires when every slot is filled.
patternstring (RegExp source)Restrict input, e.g. REGEXP_ONLY_DIGITS.
InputOTPSlot
A single character cell; reads its char/caret/active state from OTPInputContext by index.
indexnumberThe slot's position among all slots (required).
InputOTPSeparator
A decorative divider between InputOTPGroups (e.g. 3 + 3 for a 6-digit code).
classNamestringNo dedicated props — renders a fixed dash icon; accepts standard div attributes.
Accessibility
Built to be keyboard-navigable and screen-reader friendly out of the box.
| Key | Function |
|---|---|
| Arrow+Left | Moves to the previous slot. |
| Arrow+Right | Moves to the next slot. |
| Backspace | Clears the current slot and steps back. |
- Renders a single hidden input — paste fills every slot at once.
- Set autoComplete="one-time-code" so mobile keyboards offer the SMS code.
- Always pair with a visible instruction telling users where the code came from.
Guidelines
Conventions that keep usage consistent across an app.
Do
- Match the slot count to the real code length.
- Group long codes (3 + 3) with a separator for readability.
Don’t
- Don’t use OTP slots for general text — use Input.
- Don’t block paste; many users paste the code from another app.
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.