A11y OTP Field

Progressive enhancement for one-time codes

One native input. Clear verification-code slots.

A11y OTP Field keeps the real form value in one text input, then adds decorative visual slots without taking over keyboard editing, paste, or one-time-code autofill.

Enter six digits or paste your code.

Ready for paste and one time code autofill

Quick facts

Form value
One native input
Runtime
Browser
Source
TypeScript
Dependencies
None at runtime

Live demo

Try native editing, autofill, and paste

Enter six digits, or paste 123-456. Use the same selection, arrow, Home, End, Backspace, Delete, and clipboard commands as any text field.

Enter the six-digit demo code.

This local demo does not send, store, or verify the code.

Why this approach

Native behavior stays in charge

The component enhances author-provided markup instead of replacing a text field with a row of separate controls.

One semantic value

Labels, form submission, selection, and editing remain attached to a single native input.

Decorative presentation

Generated slots are marked aria-hidden="true" and never become extra focus stops.

Input that stays native

Keyboard commands and paste remain available, while numeric and alphanumeric values can be normalized.

Resilient by default

The labeled text field remains usable without JavaScript, and auto-submit stays disabled unless enabled explicitly.

Example gallery

Explore a focused behavior

Each page isolates one configuration or state so you can inspect the markup, interaction, and fallback without unrelated UI.

Setup

Build locally, then enhance explicitly

The package is prepared for npm but is not public yet. Until it is published, clone the repository, install its development dependencies, and build the distributable files locally.

Run the examples

git clone https://github.com/vmitsaras/A11y-OTP-Field.git
cd A11y-OTP-Field
npm install
npm run build:dist
python3 -m http.server 4173

Initialize one field

import { createA11yOtpField } from "a11y-otp-field";
import "a11y-otp-field/styles.css";

const root = document.querySelector("[data-a11y-otp]");

if (root instanceof HTMLElement) {
  createA11yOtpField(root, { length: 6 });
}

Use one labeled text input

The plugin generates the visual slots during enhancement. Do not add one input for each character.

<div class="a11y-otp" data-a11y-otp data-otp-length="6">
  <label class="a11y-otp__label" for="verification-code">
    Verification code
  </label>
  <p class="a11y-otp__hint" id="verification-code-hint">
    Enter the six-digit code.
  </p>
  <div class="a11y-otp__control" data-a11y-otp-control>
    <input
      class="a11y-otp__input"
      data-a11y-otp-input
      id="verification-code"
      name="verificationCode"
      type="text"
      inputmode="numeric"
      autocomplete="one-time-code"
      maxlength="6"
      aria-describedby="verification-code-hint"
    />
  </div>
  <p class="a11y-otp__error" data-a11y-otp-error></p>
  <p class="a11y-otp__status" data-a11y-otp-status aria-live="polite"></p>
</div>

Configuration

A small API for real OTP flows

Options can come from JavaScript or matching data-otp-* attributes. Defaults favor predictable manual submission.

A11y OTP Field options
OptionDefaultPurpose
length6Sets the accepted code length, up to 64 characters.
typenumericAccepts numeric, alphanumeric, or custom-normalized values.
groupsnullGroups slots visually without splitting the form value.
privacyModevisibleControls visible, masked, or reveal-last presentation.
validateOnsubmitRuns validation on submit, blur, or only when requested.
autoSubmitfalseOpts into delayed form submission after completion.
messagesBuilt inOverrides validation and status text.

Instances

getValue(), setValue(), clear(), focus controls, state setters, validate(), and destroy().

Events

The root dispatches bubbling init, input, change, complete, incomplete, paste, error, clear, and destroy events under the a11y-otp-field: namespace.

Accessibility behavior

Enhancement without a new interaction model

The component relies on native text-input behavior and keeps visual presentation out of the accessibility tree.

Included behavior

  • A labeled native type="text" input owns the value.
  • inputmode and autocomplete="one-time-code" are preserved or added.
  • Slots are decorative and receive aria-hidden="true".
  • Errors synchronize visible text, aria-invalid, and descriptions.
  • Forced-colors and reduced-motion preferences receive dedicated CSS behavior.

Keyboard and clipboard

Tab
Moves to the single input and other native controls.
Arrow, Home, End
Move the native text selection and caret.
Backspace, Delete
Edit the value with native text-field behavior.
Paste
Remains available; valid characters are normalized afterward.

Styling

Customize public CSS variables

Override public properties on .a11y-otp. Variables prefixed with --_ are internal and are not customization hooks.

Theme example

.checkout-code {
  --a11y-otp-slot-size: 3rem;
  --a11y-otp-slot-gap: 0.625rem;
  --a11y-otp-focus-color: #075fe4;
  --a11y-otp-error-color: #a61b29;
  --a11y-otp-radius: 0.75rem;
}

Useful hooks

  • --a11y-otp-slot-size
  • --a11y-otp-slot-gap
  • --a11y-otp-group-gap
  • --a11y-otp-border-color
  • --a11y-otp-focus-color
  • --a11y-otp-error-color
  • --a11y-otp-radius

Implementation responsibilities

What the component does not do

Inspect the implementation

Start with the source, then test the complete flow

Review the package API, tests, demo states, and fallback behavior before integrating a verification service.

View the GitHub repository