OTP Field
A one-time password input composed of individual character slots.
Installation
pnpm dlx shadcn@latest add @lumi-ui/otp-field
import { OTPField } from "@/components/ui/otp-field"<OTPField length={6} />Anatomy
<OTPField length={6} />Size
Alphanumeric
Use validationType="alphanumeric" for recovery, backup, or invite codes that mix letters and numbers. See Validation types for the full enum.
Grouped
Wrap subsets of inputs in your own layout elements and use <OTPFieldSeparator> when you want the code presented in smaller visual chunks such as 123-456. Requires primitive usage — the composite renders inputs only.
Masked
Use the mask prop to obscure every slot, or pass type="password" to individual <OTPFieldInput> parts for per-slot control.
<OTPFieldRoot length={6}>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
<OTPFieldInput type="password" />
</OTPFieldRoot>Placeholder hints
<OTPFieldInput> is a real input, so native placeholder props and CSS work as usual. This example keeps placeholder hints visible until the active slot receives focus.
Form controls must have an accessible name. Prefer using <Field> to provide a visible text label and description, or use the aria-label attribute as an alternative. See Accessibility.
Accessibility
Every OTP field must have an accessible name. Choose one of the two patterns below.
With a native label
Pass id to <OTPFieldRoot> and pair it with <label htmlFor>. The first slot picks up the label automatically; add aria-label to remaining slots so screen readers announce which character is focused. Use aria-describedby for supporting text.
<label htmlFor="verification-code">Verification code</label>
<OTPFieldRoot
id="verification-code"
length={6}
aria-describedby="verification-code-help"
>
<OTPFieldInput />
<OTPFieldInput aria-label="Character 2 of 6" />
<OTPFieldInput aria-label="Character 3 of 6" />
<OTPFieldInput aria-label="Character 4 of 6" />
<OTPFieldInput aria-label="Character 5 of 6" />
<OTPFieldInput aria-label="Character 6 of 6" />
</OTPFieldRoot>
<p id="verification-code-help">
Enter the 6-character code we sent to your device.
</p>The composite <OTPField> already adds the per-slot aria-labels — when using it, you only need id + <label>.
With Field
Use <Field> to handle label association, descriptions, and form validation automatically:
<Field name="verificationCode">
<FieldLabel>Verification code</FieldLabel>
<FieldDescription>
Enter the 6-digit code we sent to your device.
</FieldDescription>
<OTPField length={6} required />
<FieldError />
</Field>Custom sanitization
Set validationType="none" with sanitizeValue to normalize pasted values before they reach state. Use inputMode when a custom rule still needs a specific virtual keyboard hint, and onValueInvalid to react to rejected characters. This example accepts digits 0-3 only, shakes the focused slot on rejection, and announces what was dropped via aria-live.
sanitizeValue is silently ignored when validationType is 'numeric', 'alpha', or 'alphanumeric' — the built-in sanitizer wins. To combine built-in rules with custom logic, set validationType="none" and re-implement the rule inside your sanitizer.
The demo ships its keyframes inline via a <style> tag so it's drop-in. For production, lift the @keyframes otp-shake-a/b and .otp-shake-a/b rules into your global CSS (or a Tailwind plugin) and remove the <style> element.
With Form
Pass autoSubmit to submit the owning form automatically when all slots are filled, or use onValueComplete to react to completion without submitting. See Complete & autoSubmit for the callback order.
See Form Integration for more information.
Native form submission
The form value is a single string of length length keyed by name on <OTPFieldRoot>:
<form action="/verify" method="post">
<OTPField length={6} name="code" required />
</form>
// POST body: { code: "123456" }Combine with autoSubmit to skip the explicit submit button entirely. For client-side libraries (React Hook Form, Conform, etc.), drive the field with value + onValueChange and validate against value.length === length.
Controlled vs uncontrolled
<OTPField defaultValue="123" length={6} onValueComplete={handleComplete} />const [value, setValue] = useState("");
<OTPField length={6} value={value} onValueChange={setValue} />value and defaultValue are mutually exclusive. The value is always a single string up to length characters — never an array. The string only reaches length === value.length when the field is complete.
Complete & autoSubmit
Callback order on each keystroke or paste that fills the field:
onValueChange(value, { reason })— fires for typing, paste, clear, or keyboard nav that mutates the value.onValueComplete(value, { reason })— fires oncevalue.length === length, after the state update is applied.reasonis'input-change' | 'input-paste'.- Form submit — only if
autoSubmitis set. Runs immediately afteronValueComplete.
onValueComplete fires whether or not autoSubmit is enabled — use it to advance a wizard step, call a verify API, or close a dialog without submitting a form.
API Reference
Components
OTPFieldRoot Props
OTPFieldInput Props
Renders an <input> element. Accepts all native input attributes (type, placeholder, aria-label, onFocus, etc.) plus the Base UI render-prop trio: className, style, render.
The wrapper in this registry adds an inputSize variant: 'sm' | 'default' | 'lg'.
Validation types
State data attributes
<OTPFieldRoot> exposes:
<OTPFieldInput> exposes a per-slot data-filled plus all of the Root attributes (mirrored for styling convenience).