Liking cljdoc? Tell your friends :D

x-otp-input

A native Web Component for entering one-time passwords / verification codes. Renders a row of single-character "slot" inputs with auto-advance, paste distribution, and full keyboard navigation. Form-associated, so it works inside <x-form> automatically.

Tag name

<x-otp-input></x-otp-input>

Observed attributes

AttributeTypeDefaultDescription
namestring""Form field name. Read by <x-form> when collecting submit values.
valuestring""Current code. Always normalized: filtered by type and truncated to length.
lengthnumber6Number of slots. Clamped to [1, 12].
typeenumnumericAllowed character set: numericalphanumericalpha.
maskbooleanfalseRenders each slot as a password input (dots).
disabledbooleanfalseDisables all slots and prevents form submission.
readonlybooleanfalseDisplay only — no edit.
requiredbooleanfalseRequired for form submission. Fails validity unless fully filled.
autofocusbooleanfalseFocuses the first empty slot on connect.
labelstring""aria-label for the slot group.
placeholderstring""Single-character placeholder shown in each empty slot.
errorstring""Error message — sets aria-invalid on slots and a customError validity flag. Set by x-form.setFieldError.

Boolean attributes follow standard HTML conventions: presence means true, absence means false. Length/type changes rebuild the slot DOM; all other attribute changes patch existing slots.


Properties

PropertyTypeReflects attribute
namestringname
valuestringvalue
lengthnumberlength
typestringtype
maskbooleanmask
disabledbooleandisabled
readonlybooleanreadonly
requiredbooleanrequired
autofocusbooleanautofocus
labelstringlabel
placeholderstringplaceholder
errorstringerror

Setting value programmatically distributes characters across slots and triggers a re-render. Setting length or type rebuilds the slot DOM.


Methods

MethodSignatureDescription
focus()() → voidFocuses the first empty slot. If all slots are filled, focuses the last slot.
clear()() → voidClears the value and focuses slot 0.
checkValidity()() → booleanProxies to ElementInternals.checkValidity().
reportValidity()() → booleanProxies to ElementInternals.reportValidity() — surfaces native validation UI on invalid state.

Events

EventCancelableDetailWhen
x-otp-input-inputno{name, value, complete}On every keystroke or paste. complete is true when value reaches length.
x-otp-input-changeno{name, value, complete}When focus leaves the component, if the value changed since focus entered.
x-otp-input-completeno{name, value}Once, when the user fills the last empty slot.

Slots

This component does not expose any light-DOM slots. The visible slot inputs are constructed inside the shadow root.


CSS custom properties

PropertyDefaultDescription
--x-otp-input-slot-size2.75remWidth and height of each slot box.
--x-otp-input-gapvar(--x-space-sm, 0.5rem)Gap between slots.
--x-otp-input-bgvar(--x-color-surface, #ffffff)Slot background.
--x-otp-input-colorvar(--x-color-text, #111827)Slot text colour.
--x-otp-input-border1px solid var(--x-color-border, #d1d5db)Slot border.
--x-otp-input-border-radiusvar(--x-radius-md, 6px)Slot corner radius.
--x-otp-input-focus-ring-colorvar(--x-color-primary, #2563eb)Focus border + ring colour.
--x-otp-input-error-colorvar(--x-color-danger, #dc2626)Error border + ring colour.
--x-otp-input-disabled-opacityvar(--x-opacity-disabled, 0.45)Opacity for the disabled state.
--x-otp-input-font-familyvar(--x-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)Slot font.
--x-otp-input-font-size1.25remSlot font size.
--x-otp-input-font-weightvar(--x-font-weight-semibold, 600)Slot font weight.

Parts

PartElement
rootThe wrapper <div role="group"> containing all slots.
slotEach slot <input> element.

Use ::part(slot) to style slots from outside the shadow root, e.g. x-otp-input::part(slot) { letter-spacing: 0; }.


Shadow DOM structure

<div part="root" role="group" aria-label="…">
  <input part="slot" data-index="0" maxlength="1" inputmode="numeric"
         autocomplete="one-time-code" autocapitalize="off" autocorrect="off"
         aria-label="Digit 1 of 6" />
  <input part="slot" data-index="1" maxlength="1" inputmode="numeric"
         autocomplete="off" autocapitalize="off" autocorrect="off"
         aria-label="Digit 2 of 6" />
  …
</div>

The first slot uses autocomplete="one-time-code" so iOS/Safari and Chrome on Android pick up SMS codes automatically. Subsequent slots use autocomplete="off" to avoid autofill conflicts.


Keyboard interaction

KeyEffect
Type a characterInserts into focused slot, then auto-advances to the next slot.
BackspaceIf the focused slot is empty, clears the previous slot and moves focus there. Otherwise, native delete behaviour.
Move focus to the previous slot.
Move focus to the next slot.
HomeMove focus to slot 0.
EndMove focus to the last slot.
PasteDistributes pasted text across slots starting from the focused slot, filtering invalid characters per type.

Validity

The component is form-associated via ElementInternals:

  • Empty + requiredvalueMissing flag, message: "Please fill in this field."
  • Partial + requiredtooShort flag, message: "Please enter all N characters."
  • error attribute setcustomError flag with the error message as text.
  • All other states are valid.

<x-form> calls reportValidity() before dispatching x-form-submit, so an invalid OTP blocks submission and shows native validation UI.


Accessibility

  • Wrapper has role="group" with aria-label from the label attribute.
  • Each slot has aria-label="Digit N of M" (or "Character N of M" for non-numeric types).
  • The first slot gets aria-required="true" when required is set.
  • All slots get aria-invalid="true" when error is set.
  • The component honours prefers-reduced-motion (no focus-ring transition) and prefers-color-scheme: dark.
  • Touch targets are at least 44×44px on coarse pointers.

Usage examples

Basic 6-digit code

<x-otp-input id="otp" label="Verification code" autofocus></x-otp-input>

<script>
  document.getElementById('otp').addEventListener('x-otp-input-complete', e => {
    console.log('Got code:', e.detail.value);
  });
</script>

Inside <x-form>

<x-form id="verify-form">
  <x-otp-input name="code" length="6" required label="Enter the code we texted you"></x-otp-input>
  <button type="submit">Verify</button>
</x-form>

<script>
  document.getElementById('verify-form').addEventListener('x-form-submit', async e => {
    try {
      await api.verify(e.detail.values.code);
    } catch (err) {
      // Server-side error injection — sets error attribute on the OTP input.
      document.getElementById('verify-form').setFieldError('code', err.message);
    }
  });
</script>

Alphanumeric, masked, custom length

<x-otp-input
  name="recovery"
  type="alphanumeric"
  length="8"
  mask
  placeholder="·"
  label="Recovery code">
</x-otp-input>

Programmatic API

const otp = document.querySelector('x-otp-input');
otp.value = '123456';   // distributes across slots
otp.focus();             // focuses first empty slot
otp.clear();             // clears value, focuses slot 0

Custom theming

x-otp-input {
  --x-otp-input-slot-size: 3.5rem;
  --x-otp-input-gap: 0.75rem;
  --x-otp-input-font-size: 1.5rem;
}

x-otp-input::part(slot) {
  letter-spacing: 0;
}

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close