Skip to content

OTP Input

Authentication UISince 0.3.0
import { OtpInput } from '@absoluteui/components/otp-input';
import '@absoluteui/components/otp-input/style.css';

Using Angular? See OTP Input for Angular.

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-complete event and the FormControl contract.

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

  • SMS, email, or authenticator codes.
  • Numeric PINs (with mask).
  • Recovery codes (alphanumeric with groupSize).
  • Passwords: use Password Input.
  • Free-form identifiers of varying length: use Text Input.

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();
});
}
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' });
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();
});
<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>
OtpInput.create(host: HTMLElement, options: OtpInputOptions): OtpInput

Global configuration key: otpInput (see Configuration).

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

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.
Slot Default icon
error alert-circle

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

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 }

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.

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

Criterion Level Note
1.3.1 Info and Relationships A
1.3.5 Identify Input Purpose AA
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.7 Focus Visible AA
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
3.3.8 Accessible Authentication (Minimum) AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

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

empty, partially filled, complete, focused (active cell), masked, disabled, read-only, required, invalid.

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)
  • 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.

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.

Codes are left-to-right in every locale: the input and cells use dir="ltr". The label and messages follow the page direction.

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.

configureAbsoluteUI({ components: { otpInput: { validationTiming: 'submit' } } });

Instance options override global and theme defaults. All options are dynamic.

icons.error (default alert-circle).

  • Characters are filtered to the allowed set after NFKC normalization and rendered with textContent.
  • Events never include the code; the component never logs.
  • mask shows dots and switches the input to type="password".
  • Rate limiting, expiry, and verification of codes belong to your server; AbsoluteUI is not an authentication provider.

The code lives only in the native input. No persistence, logging, network access, or telemetry.

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

Codes of 1-32 characters.

  • 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 to length.
  • Changing length or type re-normalizes the current code.
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.

  • ASCII letters and digits only.
  • Pointer-drag selection is not mirrored cell by cell.
  • Manual screen-reader verification is pending.
  • N separate inputs (deliberately not offered).
  • Custom character sets.