# OTP Input (/docs/inputs/otp-input)



<ComponentShowcase name="otp-input" />

## Installation [#installation]

```bash
npx shadcn@latest add https://ui.zyeon.ai/r/otp-input.json
```

## Prompt [#prompt]

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

```text
Build a React + TypeScript + Tailwind "OtpInput" component (no extra libraries).

Contract
- Export a forwardRef div (role="group", aria-label from `label`, default
  "Verification code") extending HTMLAttributes minus onChange/defaultValue.
- Props: length (default 6), groupSize?, value?/defaultValue (controlled or
  uncontrolled string), onChange?(value), onComplete?(value),
  pattern ("numeric" | "alphanumeric", default "numeric"), disabled, invalid,
  autoFocus, mask, label.
- The value is one plain string; each cell renders value[i] ?? "". Anything
  past `length` is clipped, so a parent can never overfill the field.

Behavior
- One <input maxLength=1> per cell. Typing writes the character and moves
  focus to the next cell; the last cell keeps focus.
- Every incoming character — typed, pasted, or injected by autofill — is run
  through the pattern regex first. Rejected characters change no state, and
  React restores the cell's displayed value on its own.
- onPaste preventDefaults, filters the clipboard text and writes it from the
  focused cell rightwards, spilling into the following cells; focus lands
  after the last cell it filled. A multi-character change event (SMS autofill)
  takes the same path, so only cell 0 needs autoComplete="one-time-code"
  (the rest are autoComplete="off").
- Backspace: clears the current cell if it holds a character, otherwise
  clears the previous cell and moves focus there. Delete clears in place.
  ArrowLeft/ArrowRight move focus, Home/End jump to the first/last cell.
- Gap-free focus: user focus (click or Tab) past the first empty cell is
  redirected to that first empty cell, and ArrowRight / End clamp the same
  way — the code can never grow holes, so joining the cells into one string
  is always faithful. The component's own auto-advance is exempt from the
  redirect (a ref flags it), since it computes its target from fresh state
  while a focus handler would still be reading the previous render.
- Focus selects the cell's contents — on focus, on click (mouseup collapses the
  selection made on focus) and inside the focus helper itself, because moving to
  the cell that already has focus fires no focus event. Without that last one the
  last cell of a full code keeps its caret behind the character and maxLength
  swallows every further keystroke, so the digit could only be changed by
  backspacing first. Selecting means typing always overwrites.
- onComplete fires on the transition into a full code, tracked with a ref of
  the last announced value: editing a complete code does not re-fire it, and
  dropping below full arms it again.
- mask renders the cells as type="password"; inputMode stays "numeric" for
  numeric patterns so mobile keeps the number pad.
- disabled disables every cell and dims the group.

Rendering & styling
- Semantic tokens only: border-input with focus:border-ring +
  focus:ring-ring/50 normally, border-destructive + focus:ring-destructive/40
  plus aria-invalid on every cell when invalid; the group separator is
  bg-border; the caret is bg-foreground.
- Each cell is size-11, rounded-md, text-center, tabular-nums.
- groupSize inserts a small bar between groups (length 6 + groupSize 3 →
  three cells, separator, three cells) rendered aria-hidden.
- The native caret is hidden (caret-transparent) and replaced by a 1px bar
  that blinks via a hoisted @keyframes in a React 19
  <style href precedence="medium"> tag; it is motion-reduce:[animation:none]
  so reduced-motion users get a steady caret instead of no caret.
- Each cell carries aria-label "Digit N of M" ("Character N of M" for
  alphanumeric). Merge the consumer className via cn() on the group.

Customization levers
- Length and grouping: length + groupSize cover 4-digit PINs, 6-digit TOTP
  and 8-character backup codes without touching the logic.
- Cell size/density: size-11 and the gap-2 on the group are the two knobs;
  scale them together (size-9/gap-1.5 compact, size-14/gap-3 for large text).
- Pattern: PATTERNS is a map of regexes — add "alpha" or a hex-only entry and
  the filter, paste and autofill paths all follow.
- Separator: swap the bar for a "–" span or drop groupSize for one run.
- Auto-submit: call your verify mutation from onComplete and drive `invalid`
  from its result; clear the value to let the user retry.
- Caret: remove the fake caret and drop caret-transparent if you prefer the
  native caret (you then lose the reduced-motion freeze).
```

## Concepts [#concepts]

<Mermaid
  chart="`flowchart TD
A[&#x22;keystroke / paste / SMS autofill&#x22;] --> B[&#x22;filter through the pattern regex&#x22;]
B --> C[&#x22;write from the focused cell<br/>overflow spills into later cells&#x22;]
C --> D[&#x22;cells joined into one string&#x22;]
D --> E[&#x22;onChange(value)&#x22;]
D --> F{&#x22;every cell filled?&#x22;}
F -->|&#x22;yes, first time&#x22;| G[&#x22;onComplete(value)&#x22;]
F -->|&#x22;no&#x22;| H[&#x22;re-arm onComplete&#x22;]
I[&#x22;user focuses a cell&#x22;] --> J{&#x22;past the first empty cell?&#x22;}
J -->|&#x22;yes&#x22;| K[&#x22;redirect to the first empty cell&#x22;]
J -->|&#x22;no&#x22;| L[&#x22;select contents<br/>fake caret blinks&#x22;]`"
/>

* **Auto-advance and backspace-rewind** — typing pushes focus forward one cell, Backspace clears in place and only steps back once the cell is already empty; the pair is what makes a segmented field feel like a single text input.
* **The caret always lands on a selection** — whichever cell the caret settles in has its contents selected, including the case where it never actually moved (the last cell of a full code), so any digit can be retyped in place instead of forcing a Backspace first.
* **Paste distribution** — a pasted (or SMS-autofilled) string is filtered and spread across the cells from the focused one, so the everyday "copy the code from the email" gesture fills the whole field in one action.
* **Gap-free focus** — user focus can never land past the first empty cell, which guarantees the cells hold a contiguous code and lets the component keep a plain string as its value instead of a sparse array.
* **Completion edge, not completion state** — `onComplete` fires on the transition into a full code (tracked by a ref), so auto-submit runs once rather than on every keystroke after the code is full.
* **Filter, don't fight** — illegal characters simply produce no state change; React's controlled-input reconciliation restores the cell, which is cheaper and steadier than intercepting every keydown.
* **Fake caret for reduced motion** — the native caret is hidden and redrawn as a blinking bar, the one way to honour `prefers-reduced-motion` (a native caret blinks no matter what CSS says) while keeping a visible insertion point.
