Input OTP
Accepts a one-time password one character per slot, with paste and keyboard support.
Installation
Install the following dependencies:
Set up the token layer and the StyleX compiler, if you have not already. See
Installation, and paste states.ts from the same page. A
slot's field surface, its focus ring and its invalid ring come from it, and so does the dim a
disabled field takes. The field also needs interop.css, which sizes the
separator's glyph.
Paste field-state.ts and state-styles.ts from the same installation page.
Copy and paste the following code into your project.
Update the import paths to match your project setup.
Usage
Composition
A slot takes no index. It reads its position from where it renders, so the
slots take their order from the markup, and length on the root is the only
count.
Basic
Separator
Put InputOTPSeparator between two groups. It renders a dash; pass a child to
use a different glyph.
Validation type
Use validationType to choose what the field accepts. It also picks the
virtual keyboard, which inputMode overrides.
Mask
Use the mask prop to obscure the code on a shared screen.
Disabled
Set disabled on Field to disable its control and dim its label, or on a bare control directly.
Controlled
Use value and onValueChange to control the field. The second argument
carries a reason: input-change, input-clear, input-paste or keyboard.
Complete
onValueComplete fires once every slot is filled, after the value has been
applied. Set autoSubmit to submit the owning form at the same moment.
Invalid
Set invalid on Field to style its control, or aria-invalid on a bare control. The examples show both.
This is Base UI's OTPField, not shadcn's input-otp wrapper. Every slot is a
real <input>, so the browser handles the caret, the text selection and the
mobile keyboard. There is no fake blinking caret to render and no hidden input
sitting over the slots.
API reference
Pass render to replace a part's element or compose it with another component.
Every part wraps the Base UI OTPField part of the same name and accepts its
props. See the Base UI reference.
Each part also takes xstyle and className.
InputOTP
| Prop | Type | Default |
|---|---|---|
length | number | required |
value | string | — |
defaultValue | string | — |
onValueChange | (value: string, details) => void | — |
onValueComplete | (value: string, details) => void | — |
onValueInvalid | (value: string, details) => void | — |
validationType | "numeric" | "alpha" | "alphanumeric" | "none" | "numeric" |
mask | boolean | false |
autoSubmit | boolean | false |
invalid | boolean | false |
disabled | boolean | false |
readOnly | boolean | false |
required | boolean | false |
name | string | — |
autoComplete | string | "one-time-code" |
normalizeValue | (value: string) => string | — |
length is required. The root clamps the value, detects completion and renders
the hidden validation input from it, all before the slots hydrate.
Set invalid on a bare OTP. Inside Field, slots inherit validity without that prop.
InputOTPGroup
A row of slots. The group squares its slots' inner corners and drops their shared borders, so a slot rendered outside a group stays a box of its own.
InputOTPSlot
One character. Takes every <input> attribute and Base UI's render. Its
aria-invalid also accepts an explicit invalid state on a bare slot.
InputOTPSeparator
Base UI's Separator, carrying role="separator". The default child is a
lucide Minus.
inputOTPStyles
The style maps behind the component. Spread them onto any element.
| Key | What it is |
|---|---|
base | The root row and its gap |
disabled | The dimmed, non-interactive state |
group | A run of slots, carrying the drop once |
slot | One slot: box, type, field surface, stacking order |
slotJoined | A member's squared corners, shared border, no drop |
slotInvalid | The destructive border and ring |
separator | The glyph between two groups |
slot is three styles, in merge order: the slot's own, the shared
states.focusRing and states.fieldSurface. slotInvalid is invalidRing and
disabled is disabled, both from states in theme/states.ts. Set a slot
border of your own through --carbon-border-color rather than borderColor. A
flat borderColor replaces the property with all of its conditions, so the ring loses its focus colour.
Data attributes
| Attribute | On | Effect |
|---|---|---|
data-slot="input-otp" | the root | Targets the field from a parent |
data-slot="input-otp-group" | a group | Targets a group from a parent |
data-slot="input-otp-slot" | a slot | Targets a slot from a parent |
data-slot="input-otp-separator" | a separator | Targets a separator from a parent |
data-complete | the root | Present once every slot is filled |
data-filled | root, slot | Present when the value, or that slot, has a character |
data-disabled, data-readonly, data-required | root, slot | Base UI writes them |