Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Input OTP

A one-time-password field with per-character slots — copy/paste aware, patterned, separable, and driven by one hidden input.

Status
stable
Since
0.10.0
Accessibility pattern
single native input behind the slots

Last updated

Install

Add Input OTP from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/input-otp

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

InputOTP — data-slot="input-otp"
InputOTPGroup — data-slot="input-otp-group"
InputOTPSlot — data-slot="input-otp-slot"
InputOTPSeparator — data-slot="input-otp-separator"

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.

1
2
3
4
5
6

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
    └── InputOTPSlot

Pattern

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.

1
2
3
4
5
6

Controlled

Hold the value yourself with value and onChange.

Enter your one-time password.

Invalid

Put aria-invalid on the slots. The tint is scoped to not-data-[active=true]: (FOC-5), so the slot the caret is in shows its focus tint rather than the error colour.

0
0
0
0
0
0

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.

We sent a six-digit code to your email.

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.

1
2
3
4
5
6
1
2
3
4
5
6

API Reference

PropTypeDefaultDescription
containerClassNamestring
PropTypeDefaultDescription
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 paints the border tint the rest of the text-entry family uses on :focus (FOC-3) — 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: FieldLabel bound with htmlFor, or an aria-label.
ContractStates tested
Behaviourdefault, active, filled, disabled, invalid
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, invalid, disabled
Visualdefault, active, invalid, disabled

Do / Don't

Do
Use maxLength and a pattern that match the code you actually send, so a wrong character is rejected before submit.
Don't
Render one Input per character — paste, autofill and backspace all break.

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-ring plus ring-3 ring-ring/50 becomes data-[active=true]:border-ring/70: the same border tint the rest of the text-entry family uses, and no glow.
  • FOC-5 — the invalid tint becomes not-data-[active=true]:aria-invalid:border-destructive, so the slot holding the caret shows its own cue; the group's has-aria-invalid halo rings are removed and its border tint becomes not-focus-within:has-aria-invalid:border-destructive.
  • DOC-2cn is imported from @vegastack/design.

On this page