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
| 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