Input OTP
A one-time-password field with per-character slots — copy/paste aware, patterned, separable, and driven by one hidden input.
- Status
- Since
0.10.0- Accessibility pattern
- single native input behind the slots
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Input OTP before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify input-otpInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/input-otpRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
It also adds the sanctioned engine to your package.json: input-otp (one-time-password field state machine).
Usage
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
</InputOTPGroup>
</InputOTP>;Anatomy
Examples
About
The slots are presentation. A single hidden <input> behind them owns the value, so paste, browser
autofill and the platform's SMS suggestion all land in the right cells, and the caret never has to
be managed by hand. It is input-otp by Guilherme Rodz, the engine shadcn ships under every style.
One hidden input drives every slot — paste, autofill and the mobile SMS suggestion all land in the right cells.
Composition
InputOTP
├── InputOTPGroup
│ └── InputOTPSlot
├── InputOTPSeparator
└── InputOTPGroup
└── InputOTPSlotPattern
pattern rejects characters as they are typed. input-otp exports REGEXP_ONLY_DIGITS,
REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS.
Letters are rejected as you type.
Separator
InputOTPSeparator divides the groups; it is role="separator" and carries no text.
Disabled
Use disabled. The container dims with has-disabled, and the control keeps
disabled:cursor-not-allowed.
Controlled
Hold the value yourself with value and onChange.
Invalid
Put aria-invalid on the slots. The destructive border holds in every state, the active slot
included (FOC-14): focus never replaces it with the darker focus border.
Four Digits
The common PIN shape — four slots, digits only.
Alphanumeric
REGEXP_ONLY_DIGITS_AND_CHARS accepts letters and numbers.
Form
Inside a Field, with a description and a submit button.
RTL
A code is read left to right in every locale, so the slots keep their order; the group's rounding
mirrors with first:rounded-s-lg / last:rounded-e-lg.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
containerClassName | string | — |
| Prop | Type | Default | Description |
|---|---|---|---|
index* | number | — |
InputOTPGroup and InputOTPSeparator add no props of their own — each accepts everything a
<div> accepts.
Accessibility
- One real
<input>holds the value and the focus; the slots are<div>s that mirror its state. That is what makes paste, autofill and the mobile code suggestion work. - The active slot is marked with
data-active, which darkens its border subtly (border-ring/50, easing in over 150ms) — the same cue the rest of the text-entry family shows on focus (FOC-3, and FOC-14's text-entry exception, MK 2026-09-27). No slot changes its background, an invalid slot keeps its destructive border while active, and there is no ring anywhere (FOC-6). The hidden input suppresses its own outline inline, so nothing paints a ring around the whole overlay either. - Each slot is 32×32, comfortably past the 24px target floor (A11Y-2).
- Give the field a label:
FieldLabelbound withhtmlFor, or anaria-label.
| Contract | States tested |
|---|---|
| Behaviour | default, active, filled, disabled, invalid |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, invalid, disabled |
| Visual | default, active, invalid, disabled |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/input-otp.patch. Every hunk:
- FOC-1, FOC-3, FOC-6 — the active slot's
data-[active=true]:border-ringplusring-3 ring-ring/50is removed: no ring and no glow. - FOC-14 — the slot's invalid border is
aria-invalid:border-destructive, held in every state, the active slot included; the group'shas-aria-invalidhalo rings are removed and its invalid border ishas-aria-invalid:border-destructive. - FOC-14 text-entry exception (MK 2026-09-27) — the active slot takes
data-[active=true]:not-aria-invalid:border-ring/50, replacing its oldbg-accent/50fill, withtransition-[color,background-color,border-color] duration-150 ease-out: a subtle darker border that eases in, and no fill. Thenot-aria-invalidguard keeps the destructive border. - DOC-2 —
cnis imported from@vegastack/design.