OTP
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.
Import
import Otp from 'abaabil/otp' // structure onlyimport Otp from 'abaabil/otp/styled' // + CSSimport Otp from 'abaabil/otp/a11y' // + label/description/error wiring, onCompleteOne 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
| Prop | Type | Default | Tier | Description |
|---|---|---|---|---|
length | number | 6 | all | Number of characters. Sets maxLength and the --otp-length custom property the stylesheet sizes the strip from. |
alphanumeric | boolean | false | all | Accept [A-Za-z0-9] instead of [0-9], and show the text keyboard instead of the numeric one. |
className | string | — | all | Merged with the base class. |
label | string | — | a11y | Rendered as a real <label>, associated with the input via htmlFor/id. |
hideLabel | boolean | false | a11y | Visually hides the label; it stays a real <label> in the accessibility tree. |
description | string | — | a11y | Help text wired into aria-describedby. |
error | string | — | a11y | Error message, sets aria-invalid, wired into aria-describedby alongside the description. |
required | boolean | false | a11y | Passed to the underlying input. |
id | string | — | a11y | Overrides the generated input id. |
onComplete | (value: string) => void | — | a11y | Called 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. |