OTP Input
Segmented one-time-passcode input — a single hidden field behind rendered cells, form-associated, with SMS autofill.
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
| Field | Type | Controlled? | Writable? | Attribute mirror |
|---|---|---|---|---|
value | string | yes | yes | value |
maxLength | number | no | yes | maxlength |
pattern | string | null | no | yes | pattern |
disabled | boolean | no | yes | disabled |
required | boolean | no | yes | required |
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
| Event | detail | Bubbles | Notes |
|---|---|---|---|
input | — | yes | Native — fires on every value change. |
change | — | yes | Native — fires on every committed value change. |
complete | string | yes | Custom — fires once when the value reaches maxlength. |
Keyboard
| Key | Action |
|---|---|
| any matching character | Appends; the active cell advances. |
| any non-matching character | Blocked (when pattern is set). |
| Backspace / Delete | Removes a character. |
| ← / → / Home / End | Moves the caret; the active cell follows. |
| Paste | Runs 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:
- Label the field. Put
aria-label(oraria-labelledby) onx-otp-input; it proxies to the hidden input. - One field, not many. The cells are
aria-hidden; assistive tech sees only the input. Don't add per-cell labels or roles. autocomplete="one-time-code"+inputmodeare set for you — supply an accurateinputmodefor the character set.- Forced colors. The cell border, active ring, and caret fall back to system colors in Windows High Contrast.
- Reduced motion.
prefers-reduced-motion: reducestops 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
- Text Input — shares the size and variant tokens
- Segmented Control — single-select with an indicator
- Styles — tokens, radius, and theming