Skip to content

OTP input

A one-time-code field is a single native <input> styled to look segmented — wrap it in .re-otp-field (which is the visible box: it draws the cell dividers and owns the border + focus ring) and add .re-otp to the input. Keeping it one input means native paste, mobile SMS autofill (autocomplete="one-time-code"), IME, and form submission all just work, with no JavaScript. N separate cells would break all of those.

Set the cell count with --re-otp-length on the wrapper and maxlength on the input; pattern (e.g. [0-9]{6}) is the submit-time guard. Every cell is the same width and each digit is centered in its cell.

Live example
<form>
  <label class="re-field">
    <span class="re-field__label">One-time code</span>
    <span class="re-otp-field" style="--re-otp-length: 6">
      <input
        class="re-otp"
        type="text"
        name="code"
        inputmode="numeric"
        autocomplete="one-time-code"
        pattern="[0-9]{6}"
        maxlength="6"
        required
        data-re-otp
        data-re-otp-numeric
      />
    </span>
    <span class="re-field__hint">Enter the 6-digit code we texted you.</span>
  </label>
</form>

--re-otp-length sets the cell count; data-size="sm|lg" on the wrapper scales the cells. Use inputmode="numeric" for digits or inputmode="text" + autocapitalize="characters" for alphanumeric codes.

Live example
<label class="re-field">
  <span class="re-field__label">4-digit PIN (small)</span>
  <span class="re-otp-field" data-size="sm" style="--re-otp-length: 4">
    <input
      class="re-otp"
      type="text"
      name="pin"
      inputmode="numeric"
      pattern="[0-9]{4}"
      maxlength="4"
      data-re-otp
      data-re-otp-numeric
    />
  </span>
</label>
<label class="re-field">
  <span class="re-field__label">Alphanumeric (large)</span>
  <span class="re-otp-field" data-size="lg" style="--re-otp-length: 6">
    <input
      class="re-otp"
      type="text"
      name="ref"
      inputmode="text"
      autocapitalize="characters"
      pattern="[A-Za-z0-9]{6}"
      maxlength="6"
      data-re-otp
    />
  </span>
</label>

enhanceOtp is optional polish — it never splits the field or breaks autofill:

import { enhanceOtp } from "@relements/core/behaviors/otp";
enhanceOtp(document);

It tracks the caret to expose an active-cell hook (--re-otp-active-index), and with data-re-otp-numeric it strips non-digits on input.

Live example
<label class="re-field">
  <span class="re-field__label">Invalid</span>
  <span class="re-otp-field" style="--re-otp-length: 6">
    <input class="re-otp" type="text" value="12" maxlength="6" aria-invalid="true" />
  </span>
</label>
<label class="re-field">
  <span class="re-field__label">Disabled</span>
  <span class="re-otp-field" style="--re-otp-length: 6">
    <input class="re-otp" type="text" value="123456" maxlength="6" disabled />
  </span>
</label>

Because the field is one native <input>, accessibility is the platform’s — the segmented look is pure CSS and never reaches assistive tech.

  • Keyboard — fully native: Tab moves focus to the single input; typing, caret movement, selection, and paste all behave like a normal text field. The optional enhanceOtp adds no keys — it never auto-advances between cells or auto-submits, so there are no surprise key behaviors to learn.
  • Focus — the .re-otp-field wrapper shows the visible :focus-within ring (the inner input’s own outline is suppressed so the ring frames the whole box); an invalid field tints that ring and the border with the danger color.
  • Semantics — a screen reader announces one text input with its label (the wrapping <label>), autocomplete="one-time-code" for SMS autofill, and inputmode/pattern/maxlength/required as native constraints. aria-invalid="true" (or native :user-invalid) marks the error state; the cell dividers are a decorative ::before gradient with no role.
  • Notes — one input is the accessible choice on purpose: N separate cells would fragment the value for AT and break paste and autofill. Style the dividers with --re-color-border-strong for sufficient contrast against the surface. See the accessibility guide and pair this with the field wrapper for label and hint markup.