OTP Input
import { OtpInput } from '@absoluteui/components/otp-input';import '@absoluteui/components/otp-input/style.css';Using Angular? See OTP Input for Angular.
What it does
Section titled “What it does”Entry for one-time passcodes, PINs, MFA codes, and recovery codes. It provides:
- numeric or alphanumeric codes of 1-32 characters, shown in visual cells;
- paste distribution: a pasted code fills every cell, spaces and dashes are dropped;
autocomplete="one-time-code"for SMS and authenticator autofill;- overwrite typing, Backspace, and arrow-key navigation (native editing);
- masking for PINs, visual grouping for recovery codes, optional upper-casing;
- an
aui-completeevent and theFormControlcontract.
Design choice: one input, not N inputs. One native <input> holds the whole code; the cells are aria-hidden presentation above it that let pointer events through. N single-character inputs break one-time-code autofill and password managers (they fill one field), need focus juggling that loses characters when typing fast, and make screen readers announce N unlabelled boxes. One input keeps paste, undo, selection, and editing native, and is announced once: “Verification code, 6 digits”.
When to use
Section titled “When to use”- SMS, email, or authenticator codes.
- Numeric PINs (with mask).
- Recovery codes (alphanumeric with groupSize).
When not to use
Section titled “When not to use”- Passwords: use Password Input.
- Free-form identifiers of varying length: use Text Input.
Examples
Section titled “Examples”Six-digit MFA code that submits on completion
Section titled “Six-digit MFA code that submits on completion”import { OtpInput } from '@absoluteui/components/otp-input';
const host = document.querySelector<HTMLElement>('#code');const form = document.querySelector<HTMLFormElement>('#verify');if (host && form) { const otp = OtpInput.create(host, { label: 'Verification code', name: 'code', required: true, description: 'Enter the code from your authenticator app.' }); otp.host.addEventListener('aui-complete', () => { form.requestSubmit(); });}Recovery code in groups of four
Section titled “Recovery code in groups of four”import { OtpInput } from '@absoluteui/components/otp-input';
const host = document.querySelector<HTMLElement>('#recovery');if (host) OtpInput.create(host, { label: 'Recovery code', length: 8, type: 'alphanumeric', uppercase: true, groupSize: 4, autocomplete: 'off' });import { OtpInput } from '@absoluteui/components/otp-input';import '@absoluteui/components/otp-input/style.css';
const host = document.querySelector<HTMLElement>('#pin');if (host) OtpInput.create(host, { label: 'PIN', length: 4, mask: true, autocomplete: 'off' });More usage
Section titled “More usage”import { OtpInput } from '@absoluteui/components/otp-input';
const host = document.createElement('div');document.body.append(host);const otp = OtpInput.create(host, { label: 'Verification code', name: 'code', required: true });otp.host.addEventListener('aui-complete', () => { otp.host.closest('form')?.requestSubmit();});Anatomy
Section titled “Anatomy”<div class="aui-field aui-otp-input [--focused] [--invalid] [--disabled]"> <label class="aui-field__label" for="I">Verification code</label> <div class="aui-field__description" id="D">help text</div> <div class="aui-otp-input__control" dir="ltr"> (--aui-otp-length: 6) <input id="I" class="aui-otp-input__input" inputmode="numeric" autocomplete="one-time-code" dir="ltr" aria-describedby="D H E"> <div class="aui-otp-input__cells" aria-hidden="true"> <span class="aui-otp-input__cell aui-otp-input__cell--filled">1</span> ... <span class="aui-otp-input__separator">-</span> ... </div> </div> <span class="aui-visually-hidden" id="H">6 digits</span> <div class="aui-field__error" id="E">...</div></div>API reference
Section titled “API reference”Factory
Section titled “Factory”OtpInput.create(host: HTMLElement, options: OtpInputOptions): OtpInputGlobal configuration key: otpInput (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Visible label (required accessible name). Required. | |
hideLabel |
boolean |
false |
Visually hide the label. |
length |
number |
6 |
Number of characters (1..32). |
type |
'numeric' | 'alphanumeric' |
"numeric" |
Allowed characters. |
mask |
boolean |
false |
Show dots instead of characters (PINs); the input becomes type=password. |
uppercase |
boolean |
false |
Alphanumeric only: convert letters to upper case. |
groupSize |
number |
Visual group size (separators are presentation only). | |
value |
string |
"" |
Initial value (normalized); reset() restores it. |
name |
string |
Form field name. | |
description |
string |
Help text, referenced by aria-describedby. | |
required |
boolean |
false |
A code is required. |
disabled |
boolean |
false |
Disabled. |
readOnly |
boolean |
false |
Read-only. |
autocomplete |
'one-time-code' | 'off' |
"one-time-code" |
Autofill hint. |
validate |
(value: string) => string | null |
Consumer validation of a complete code. | |
icons |
Partial<{ error: IconRef }> |
Icon slot overrides. | |
validationTiming |
'blur' | 'input' | 'submit' |
"blur" |
When validation messages first appear; afterwards they update live. Globally configurable. |
locale |
string |
"en-US" |
Message and number-formatting locale. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Host element. |
input |
HTMLInputElement |
The native input. |
value |
string |
Current code. |
complete |
boolean |
Every cell filled. |
validity |
FieldValidity |
Current validity. |
name |
string | null |
Form field name. |
label |
string |
Accessible label (used by the error summary). |
disabled |
boolean |
Whether the control is disabled. |
dirty |
boolean |
Value differs from the initial value. |
touched |
boolean |
Focus has left the control at least once. |
options |
Readonly<OtpInputOptions> |
Current options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
getFormValue(): string |
The code. |
setValue(value: string): void |
Set the code (normalized, no events). |
clear(): void |
Empty the code and focus the field (after a rejected code). |
update(options: Partial<OtpInputOptions>): void |
Merge and apply options. |
reportValidity(): boolean |
Show messages now (marks the control submitted) and return validity. |
setCustomError(message: string | null): void |
External error (server, cross-field rule); null clears it. |
focus(): void |
Move focus to the primary focusable element. |
reset(): void |
Restore the initial value and clear dirty/touched/submitted state. |
destroy(): void |
Remove rendering, registration, listeners, and the configuration subscription. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-input |
{ validity: FieldValidity; complete: boolean } |
Yes | No | User edited the code (the code is not in the detail). |
aui-change |
{ validity: FieldValidity; complete: boolean } |
Yes | No | User committed the value (native change). |
aui-complete |
{ validity: FieldValidity } |
Yes | No | The code became complete through user input (typing, paste, or autofill). |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-otp-input |
Host (also aui-field). |
aui-otp-input--focused | --invalid | --disabled |
State modifiers. |
aui-otp-input__control |
Positioning container (dir=ltr); –aui-otp-length holds the length. |
aui-otp-input__input |
The native input (transparent text, under the cells). |
aui-otp-input__cells |
aria-hidden cell row. |
aui-otp-input__cell |
One cell (–filled, –active). |
aui-otp-input__separator |
Group separator. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
error |
alert-circle |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-lg | --aui-duration-fast | --aui-easing-standard | --aui-font-family-mono | --aui-font-family-sans | --aui-font-size-lg | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-space-1 | --aui-space-2
TypeScript definitions
Section titled “TypeScript definitions”type OtpInputType = 'numeric' | 'alphanumeric';interface OtpInputConfig { validationTiming: ValidationTiming }interface OtpInputOptions extends Partial<OtpInputConfig> { label: string; hideLabel?: boolean; length?: number; type?: OtpInputType; mask?: boolean; uppercase?: boolean; groupSize?: number; value?: string; name?: string; description?: string; required?: boolean; disabled?: boolean; readOnly?: boolean; autocomplete?: 'one-time-code' | 'off'; validate?: (value: string) => string | null; locale?: string; icons?: Partial<OtpInputIcons>;}interface OtpInputValueDetail { validity: FieldValidity; complete: boolean }interface OtpInputCompleteDetail { validity: FieldValidity }Accessibility
Section titled “Accessibility”Pattern: One native <input> with <label for>; visual cells are aria-hidden presentation. Chosen over N single-character inputs: one-time-code autofill and password managers fill one field, paste/undo/selection stay native, screen readers hear one labelled field with a length hint instead of N unlabelled boxes, and fast typing never loses characters to focus changes.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Digits / letters | Input focused | Enter a character; in a full code, typing overwrites the next character. |
| Backspace / Delete | Input focused | Native deletion. |
| ArrowLeft / ArrowRight / Home / End | Input focused | Native caret movement; the active cell follows. |
| Ctrl/Cmd+V | Input focused | Paste; a full code (spaces and dashes removed) replaces the value. |
| Element | Role |
|---|---|
| input | textbox (native); none when masked (password) |
| cells | none (aria-hidden=true) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-describedby |
input | Help text, the length hint (“6 digits”), then the visible error |
aria-invalid |
input | “true” only while an error message is shown |
aria-hidden |
cells | “true” |
The active cell shows the focus indicator (color-focus-ring border and 1px ring) while the input has focus.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- inputmode=numeric shows a numeric keypad for numeric codes.
- WCAG 3.3.8: paste and autofill are supported, so users do not have to transcribe codes.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
Behavior
Section titled “Behavior”States
Section titled “States”empty, partially filled, complete, focused (active cell), masked, disabled, read-only, required, invalid.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| empty / partial | allowed character typed | next cell filled; the active cell moves |
| partial | last cell filled (typing, paste, autofill) | complete; aui-complete |
| complete | character typed with the caret inside | the next character is overwritten; stays complete (no new aui-complete) |
| complete | Backspace | partial; the next completion emits aui-complete again |
| any | paste of a full code | value replaced (separators dropped) |
Accessibility notes
Section titled “Accessibility notes”inputmode="numeric"shows a numeric keypad for numeric codes.- WCAG 3.3.8: paste and autofill work, so users do not transcribe codes character by character.
- If you submit on
aui-complete, keep a visible submit button and show errors in the field so a mistyped code can be corrected. - Manual screen-reader verification is pending.
Responsive behavior
Section titled “Responsive behavior”Cells share the available width (up to 3rem each); 6 cells fit a 320 CSS px viewport. The input font size is at least 16 px so mobile browsers do not zoom.
Right-to-left
Section titled “Right-to-left”Codes are left-to-right in every locale: the input and cells use dir="ltr". The label and messages follow the page direction.
Theming
Section titled “Theming”Cells use the control surface tokens; the active cell uses color-focus-ring; invalid cells color-error-fg. Forced-colors mode: CanvasText borders, Highlight outline on the active cell.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { otpInput: { validationTiming: 'submit' } } });Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic.
icons.error (default alert-circle).
Security
Section titled “Security”- Characters are filtered to the allowed set after NFKC normalization and rendered with
textContent. - Events never include the code; the component never logs.
maskshows dots and switches the input totype="password".- Rate limiting, expiry, and verification of codes belong to your server; AbsoluteUI is not an authentication provider.
Data handling
Section titled “Data handling”The code lives only in the native input. No persistence, logging, network access, or telemetry.
Performance
Section titled “Performance”One input plus N spans; editing updates text of the cells only. Budget: OtpInput.create < 1 ms per instance (1,000 instances, e2e performance suite).
Supported data scale
Section titled “Supported data scale”Codes of 1-32 characters.
Edge cases
Section titled “Edge cases”- Full-width digits (123) are accepted (NFKC).
- Pasting “123 456” or “123-456” fills all cells; pasting a partial code inserts at the caret.
setValue()drops disallowed characters and truncates tolength.- Changing
lengthortypere-normalizes the current code.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
OTP_INPUT_OPTIONS |
configuration | Missing label, length outside 1-32, groupSize not less than length, uppercase without alphanumeric, invalid autocomplete. |
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION |
configuration | Invalid or unknown configuration. |
Validity error codes: value-missing (“Enter the code.”), too-short (“Enter all 6 digits.”), custom.
Known limitations
Section titled “Known limitations”- ASCII letters and digits only.
- Pointer-drag selection is not mirrored cell by cell.
- Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- N separate inputs (deliberately not offered).
- Custom character sets.