Input OTP
hc-inputotp is a segmented one-time-code field — the shadcn
InputOTP equivalent. It uses the accessible single-input approach:
one real <input autocomplete="one-time-code"> captures all typing,
paste, SMS autofill, and selection, and installInputOtp overlays it
transparently and renders the familiar boxes. Screen readers interact
with one labelled input; the slots are decorative.
This avoids the screen-reader and paste problems of the common “one input per digit” pattern.
Basic usage
Section titled “Basic usage”<div class="hc-inputotp" data-length="6"> <input class="hc-inputotp__input" type="text" name="otp" inputmode="numeric" autocomplete="one-time-code" aria-label="One-time code"></div>import { installInputOtp } from '@hypermedia-components/core';installInputOtp(); // idempotent; returns an uninstallerThe zero-config @hypermedia-components/core/behaviors entry installs
it automatically. The behavior renders the data-length slots and sets
maxlength; it also fills in inputmode="numeric",
autocomplete="one-time-code", and type="text" if you omit them
(type="text", not number, so leading zeros and paste work).
Configuration
Section titled “Configuration”| Attribute | Purpose |
|---|---|
data-length | Number of slots (default 6); also sets the input’s maxlength. |
data-pattern | Allowed characters as a CSS-style class, default [0-9]. Non-matching characters are stripped on input. Use e.g. [0-9a-zA-Z] for alphanumeric codes. |
data-groups | Visually split the slots into groups — "3-3" (also "3 3" / "2,2,2"). A decorative separator is rendered between groups; ignored unless the group sizes sum to data-length. |
The <input> carries the real value and the accessible name — always
give it an aria-label or a <label>, and a name if it should
submit.
Group separators
Section titled “Group separators”data-groups="3-3" renders a separator between each group of slots — common
for formatted codes like 123-456:
<div class="hc-inputotp" data-length="6" data-groups="3-3"> <input class="hc-inputotp__input" type="text" aria-label="One-time code"></div>The separator is aria-hidden (the input value is the source of truth). Set
--hc-inputotp-separator to any CSS content value to restyle the glyph
(e.g. "·", or "" for a plain wider gap).
States
Section titled “States”aria-invalid="true" on the input (or data-invalid on the container)
draws the error border; disabled on the input mutes the slots.
data-variant="success" | "warning" | "error" on the container
recolors the slot borders with the same validation vocabulary as the
other form fields.
Active slot & caret
Section titled “Active slot & caret”The slot the caret is in carries data-active and renders a blinking
caret (CSS, so it respects prefers-reduced-motion: reduce — the caret
stays steady instead of blinking). The active slot follows the caret: as you
type it advances to the next empty slot, and arrow keys move it within the
value.
Clicking a slot moves the caret into it so you can edit that position —
clamped to the typed length, so clicking an empty slot past the end just
parks the caret at the end (you can’t open a gap). Style the active slot via
data-active and the caret color via --hc-inputotp-caret-color.
Events
Section titled “Events”Both bubble from the container:
hc:otpchange—detail { value, input }on every edit.hc:otpcomplete— same detail, fired when the value fills every slot.
otp.addEventListener('hc:otpcomplete', (e) => { verify(e.detail.value);});htmx usage
Section titled “htmx usage”Auto-submit the code the moment it completes:
<form data-hx-post="/verify" data-hx-trigger="hc:otpcomplete"> <div class="hc-inputotp" data-length="6"> <input class="hc-inputotp__input" type="text" name="code" inputmode="numeric" autocomplete="one-time-code" aria-label="Verification code"> </div></form>Because the value lives in a single named <input>, it serialises
normally — no extra hidden fields.
Hyperscript
Section titled “Hyperscript”Auto-submit the form when the code fills up, with no helper:
<form data-hx-post="/verify"> <div class="hc-inputotp" data-length="6" _="on hc:otpcomplete call closest <form/> then call it.requestSubmit()"> <input class="hc-inputotp__input" type="text" name="code" aria-label="Code"> </div></form>More patterns: Hyperscript → Reacting to component events.
Accessibility
Section titled “Accessibility”- One real, labelled
<input>carries the value; the visual slots arearia-hiddenso assistive tech announces a single field, not N empty boxes. autocomplete="one-time-code"lets iOS / password managers offer the SMS code;inputmode="numeric"shows the numeric keypad.- The active slot’s border doubles as the focus indicator; the caret
blink respects
prefers-reduced-motion: reduce. Clicking a slot moves the caret into it for editing.
Theming tokens
Section titled “Theming tokens”| Token path | Purpose |
|---|---|
inputotp.gap | Space between slots. |
inputotp.slot-size | Slot box size (square). |
inputotp.radius / border / bg / fg | Slot chrome. |
inputotp.font-size / font-weight | The digit. |
inputotp.active-border / caret-color | Active slot + caret (track data-color). |
inputotp.error-border | aria-invalid border. |
inputotp.disabled-bg / disabled-fg | Disabled slots. |
CSS variables
Section titled “CSS variables”Show the generated CSS variables
--hc-inputotp-gap | -slot-size | -radius | -border | -bg | -fg--hc-inputotp-font-size | -font-weight--hc-inputotp-active-border | -caret-color--hc-inputotp-error-border | -disabled-bg | -disabled-fg--hc-inputotp-separator /* the data-groups separator glyph; default "–" */Out of scope
Section titled “Out of scope”RTL fine-tuning is deferred. Per-slot click caret placement (see Active slot & caret) and group separators (see Group separators) are supported.