Skip to content

Input OTP <input>

A single native <input> with visual digit cells for one-time passcode entry. The <l-input-otp> wrapper renders individual bordered cells over a hidden input that handles keyboard, paste, and autocomplete.

HTML tag<l-input-otp>
Native HTML
Progressive
Custom
Shadow DOM
Progressive Custom Element

Options

Default

Code
html
<label
  for="otp-default"
  class="block mb-1.5 text-sm font-medium"
  >Verification code</label
>
<l-input-otp>
  <input id="otp-default" />
</l-input-otp>

Digit count

Set --digits on <l-input-otp> to change the number of cells. The element automatically sets maxlength and pattern on the input.

Code
html
<label
  for="otp-pin"
  class="block mb-1.5 text-sm font-medium"
  >PIN code</label
>
<l-input-otp style="--digits: 4">
  <input id="otp-pin" />
</l-input-otp>

Separator

Add separator-after attribute to insert a visual dash after the specified position (e.g., separator-after="3" for a 3-3 grouping).

Code
html
<label
  for="otp-separator"
  class="block mb-1.5 text-sm font-medium"
  >Verification code</label
>
<l-input-otp separator-after="3">
  <input id="otp-separator" />
</l-input-otp>

Size

Set the size attribute on <l-input-otp>: sm, md (default), lg.

Code
html
<div class="flex flex-col gap-4 items-start">
  <l-input-otp size="sm">
    <input aria-label="Small OTP" />
  </l-input-otp>
  <l-input-otp>
    <input aria-label="Medium OTP" />
  </l-input-otp>
  <l-input-otp size="lg">
    <input aria-label="Large OTP" />
  </l-input-otp>
</div>

Custom colors

Override the --cell-* properties to retheme. Set --cell-focus-ring to a full box-shadow value (or none) to customize the active state. Target .l-input-otp-cell for typography tweaks.

Code
html
<style>
  #demo-otp-custom {
    --cell-size: 3.5rem;
    --cell-gap: 0.75rem;
    --cell-bg-color: white;
    --cell-border-color: oklch(0.92 0.01 270);
    --cell-border-radius: 1rem;
    --cell-focus-color: oklch(0.55 0.25 275);
    --cell-focus-ring: 0 0 0 4px color-mix(in oklab, var(--cell-focus-color) 22%, transparent);
  }

  #demo-otp-custom .l-input-otp-cell {
    font-family: inherit;
    font-weight: 700;
  }
</style>

<label
  for="otp-custom"
  class="block mb-2 text-sm font-semibold text-slate-700"
  >Label</label
>
<l-input-otp
  id="demo-otp-custom"
  style="--digits: 4"
>
  <input
    id="otp-custom"
    value="4200"
  />
</l-input-otp>

Not defined

Before JS loads (:not(:defined)), CSS reserves the exact box the cells will occupy — a single soft-tinted rectangle, not per-cell artwork. Width tracks --digits, --cell-size, and --cell-gap so layout doesn't shift on hydration, and the tint follows --cell-bg-color so custom themes carry over.

Code
html
<l-input-otp>
  <input />
</l-input-otp>

Once upgraded, the custom element replaces the input with its visual cells container.

Disabled

Native disabled attribute.

Code
html
<l-input-otp>
  <input
    value="384291"
    disabled
    aria-label="Verification code"
  />
</l-input-otp>

Accessibility

Criteria

Role

Uses a native <input> element — built-in textbox semantics

WCAG4.1.2
RGAA11.1
Accessible name

Requires <label> or aria-label to describe the purpose of the code input

WCAG1.3.1
RGAA11.1
Input purpose

autocomplete="one-time-code" identifies the field for browser and password manager autofill

WCAG1.3.5
Validation

pattern and maxlength provide client-side validation; inputmode="numeric" triggers the numeric keyboard on mobile

WCAG3.3.1
Visual cells

Cell container is aria-hidden="true" — screen readers interact with the native input only

WCAG4.1.2

Keyboard interactions

Tab
Moves focus to/from the input
0–9
Types a digit into the next available position
Backspace
Deletes the last entered digit
CtrlV / CmdV
Pastes a full code from clipboard

Why light DOM?

The native <input> lives in the light DOM so it stays directly accessible for <label> association, form participation, and external CSS. The visual cells are purely decorative (aria-hidden="true") — screen readers only see the real input.

API reference

Importing

css
@import 'luxen-ui/css/input-otp';
js
import 'luxen-ui/input-otp';

Attributes & Properties

sizeAttribute
Control size: sm, md (default), lg
separator-afterAttribute
Position after which to insert a visual separator dash (e.g., 3 for a 3-3 grouping)

CSS custom properties

Set --digits on <l-input-otp> to change the digit count.

--digitsName
Number of digit cells (default: 6). Drives maxlength and pattern automatically
--cell-sizeName
Cell width and height (default: 2.75rem). Font size scales automatically from this value
--cell-gapName
Space between cells (default: 0.5rem)
--cell-bg-colorName
Cell background color
--cell-border-colorName
Cell border color
--cell-border-radiusName
Cell border-radius
--cell-focus-colorName
Border + ring color of the active (focused) cell
--cell-focus-ringName
Full box-shadow value of the active cell ring (default: 0 0 0 1px var(--cell-focus-color)). Set to none to disable