abaabil

OTP

30th lightest of the forty, by js weight

A one-time code field: one real <input> drawn as a row of boxes. The browser supplies autofill, paste and the keyboard; the stylesheet supplies the boxes.

Example (a11y tier)

Tab to a field and type, or paste a code: the entry stops at the last box, and Backspace walks back through them.

Sent by SMS

Import

import Otp from 'abaabil/otp'         // structure only
import Otp from 'abaabil/otp/styled'  // + CSS
import Otp from 'abaabil/otp/a11y'    // + label/description/error wiring, onComplete

One input, not six

The usual code field is six inputs and a script that moves focus forward on each keystroke, splits a paste across the boxes, walks Backspace into the previous one, and tries to catch an SMS autofill that only ever lands in the first. Every one of those handlers is a place to be wrong, and the last one usually is.

This is a single text field with autoComplete="one-time-code", inputMode, pattern and maxLength. iOS, Android and password managers fill it from an SMS, the numeric keyboard comes up on a phone, the wrong characters are refused at submit, and paste, Backspace, selection and right-to-left pages all work because the browser already knows how to run a text field. There is nothing for autofill to fight.

How the boxes are drawn

The type is monospace, so every glyph is one ch wide, and letter-spacing pads each character out to a cell of one box plus one gap. The boxes themselves are three repeating gradients (top line, bottom line, both sides) with a period of one cell, painted once at exactly the strip's width so no partial box appears after the last one. The input sets --otp-length inline from length, which is how the stylesheet knows how wide the strip is.

Two properties on .abaabil-otp resize it: --otp-box-size (default --control-height-md) and --otp-gap (default --space-2). The cell, the strip, the padding and the letter-spacing are all computed from those two, so changing them keeps the glyphs centred in their boxes.

Always left to right

A code is digits and Latin letters, which read left to right in every locale. On a right-to-left page the bidi algorithm would otherwise render the run one cell in from the end edge, out of step with the boxes, so the field sets direction: ltr on itself. The label, description and error around it still follow the page.

Props

PropTypeDefaultTierDescription
lengthnumber6allNumber of characters. Sets maxLength and the --otp-length custom property the stylesheet sizes the strip from.
alphanumericbooleanfalseallAccept [A-Za-z0-9] instead of [0-9], and show the text keyboard instead of the numeric one.
classNamestring—allMerged with the base class.
labelstring—a11yRendered as a real <label>, associated with the input via htmlFor/id.
hideLabelbooleanfalsea11yVisually hides the label; it stays a real <label> in the accessibility tree.
descriptionstring—a11yHelp text wired into aria-describedby.
errorstring—a11yError message, sets aria-invalid, wired into aria-describedby alongside the description.
requiredbooleanfalsea11yPassed to the underlying input.
idstring—a11yOverrides the generated input id.
onComplete(value: string) => void—a11yCalled with the value the moment the entry reaches length, from typing, paste or SMS autofill. The field stays uncontrolled and your own onInput still runs.