Skip to content

OTP Input

A segmented passcode field. A single transparent <input> overlays the rendered cells and owns all keyboard, pointer, paste, and mobile-autofill behavior; the cells only paint the value and a caret. Because it is one field, a screen reader hears one textbox and there is no roving focus.

Install

import "elements-kit/ui/styles.css";
// a neutral + an accent scale for theming:
import "elements-kit/ui/styles/palette/slate.css";
import "elements-kit/ui/styles/neutral/slate.css";
import "elements-kit/ui/styles/palette/mint.css";
import "elements-kit/ui/styles/accent/mint.css";
// importing the module registers the elements + the css:
import "elements-kit/ui/otp-input";
import "elements-kit/ui/otp-input/otp-input.css";

Importing elements-kit/ui/otp-input registers x-otp-input, x-otp-group, x-otp-slot, and x-otp-separator.

API

Compose the root with one or more groups of indexed cells. Insert x-otp-separator between groups as you like. The number of maxlength characters is authoritative β€” give one x-otp-slot per position.

<x-otp-input name="code" maxlength="6" pattern="[0-9]" inputmode="numeric" aria-label="Verification code">
<x-otp-group>
<x-otp-slot index="0"></x-otp-slot>
<x-otp-slot index="1"></x-otp-slot>
<x-otp-slot index="2"></x-otp-slot>
</x-otp-group>
<x-otp-separator></x-otp-separator>
<x-otp-group>
<x-otp-slot index="3"></x-otp-slot>
<x-otp-slot index="4"></x-otp-slot>
<x-otp-slot index="5"></x-otp-slot>
</x-otp-group>
</x-otp-input>

By default every character is accepted. Pass pattern to restrict input β€” each character must match the regular expression. pattern="[0-9]" makes a digit-only code.

State

FieldTypeControlled?Writable?Attribute mirror
valuestringyesyesvalue
maxLengthnumbernoyesmaxlength
patternstring | nullnoyespattern
disabledbooleannoyesdisabled
requiredbooleannoyesrequired

pasteTransformer?: (text: string) => string is a property-only hook β€” return a cleaned string (e.g. strip spaces or hyphens) before the paste is validated.

Events

EventdetailBubblesNotes
inputβ€”yesNative β€” fires on every value change.
changeβ€”yesNative β€” fires on every committed value change.
completestringyesCustom β€” fires once when the value reaches maxlength.

Keyboard

KeyAction
any matching characterAppends; the active cell advances.
any non-matching characterBlocked (when pattern is set).
Backspace / DeleteRemoves a character.
← / β†’ / Home / EndMoves the caret; the active cell follows.
PasteRuns through pasteTransformer, the pattern, then maxlength.

Sizing & variants

data-size (1, 2, 3) and data-variant (surface, soft) map to the same tokens as Text Input, so an OTP field sits in the same visual family. The active cell uses the same focus color; set data-radius on a parent to change the corner rounding.

Autofill

The hidden input carries autocomplete="one-time-code", so iOS and Android offer to fill a code received by SMS. When the platform fills it, the value flows through the same validation path and paints the cells. inputmode (default text; set numeric for digit codes) picks the mobile keyboard.

Form participation

x-otp-input is a form-associated custom element. With a name, it submits its value in FormData, reflects name/value as properties (so serializers like FormObject read it), participates in reset, and reports valueMissing (when required) and tooShort validity.

const form = document.querySelector("form")!;
new FormData(form).get("code"); // "123456"

Accessibility

There is no dedicated APG pattern for a passcode field β€” it is a single masked textbox, so that is what it exposes:

  1. Label the field. Put aria-label (or aria-labelledby) on x-otp-input; it proxies to the hidden input.
  2. One field, not many. The cells are aria-hidden; assistive tech sees only the input. Don’t add per-cell labels or roles.
  3. autocomplete="one-time-code" + inputmode are set for you β€” supply an accurate inputmode for the character set.
  4. Forced colors. The cell border, active ring, and caret fall back to system colors in Windows High Contrast.
  5. Reduced motion. prefers-reduced-motion: reduce stops the caret blink.

When not to use

  • A general text field β€” use Text Input. OTP is for fixed-length codes split into cells.
  • A single-select from a short list β€” that’s the Segmented Control.

See also