OTP Field
A fixed-length code, one character per box, with paste and autofill handled.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/otp-field.jsonUsage
import { OtpField, OtpFieldInput, OtpFieldSeparator } from '@/components/ui/otp-field'<OtpField length={6} aria-label="Six-digit sign-in code">
{Array.from({ length: 6 }, (_, i) => <OtpFieldInput key={i} />)}
</OtpField>Use it for a code the user is copying from somewhere else — an SMS sign-in code, a release code on a report, an override PIN. For anything the user composes themselves, boxes are worse than a single Input: they break selection, they break the caret, and they make a typo halfway through expensive to fix.
Composition
<OtpField length={6}> {/* owns the value; length is required, not counted */}
<OtpFieldInput /> {/* one character; its index is its position */}
<OtpFieldSeparator /> {/* role=separator, not typed into */}
</OtpField>length is required and is not inferred from the children. The root has to know it before the
slots hydrate so that a pasted code can be clamped and completion detected against the
server-rendered markup. If it disagrees with the number of OtpFieldInputs you rendered, the
root wins.
Each input takes its index from its position in the DOM, so there is no index prop to pass and
no way to get it wrong.
Grouped
A separator after every third box makes a six-digit code readable in one glance instead of two. It is announced as a separator rather than skipped, and it is not in the tab order.
Letters as well as digits
validationType is 'numeric' by default and also picks the virtual keyboard —
'alphanumeric' gives a text keyboard on mobile, 'none' accepts anything. normalizeValue
runs on every value the field sees, including the default and any pasted text, which is what
makes an upper-casing normaliser safe: it has to be idempotent, and casing is.
Masked
Four-digit override PIN.
mask hides the characters. onValueComplete fires when the last slot fills, including on a
paste that completes it — which is where you kick off verification rather than waiting for a
submit button nobody is going to press.
Inside a Field
Texted to the number on file when the report was signed.
The root exposes data-complete once every slot is filled; the stylesheet uses it for a success
boundary, and Field supplies the label, description and error.
Keyboard
Typing advances, backspace retreats, arrows move between slots, and a paste anywhere fills from
the first slot. Autofill from an SMS works because the root sets
autocomplete="one-time-code" on the first slot.
API Reference
Everything Base UI's OTP Field accepts, plus:
Prop
Type
The ones you will reach for on the root:
Prop
Type