OTP Field

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJSApache-2.0
Installation mix mishka.ui.gen.headless otp_field
Source View Source

Overview of the Phoenix and LiveView Headless OTP Field Component

The Mishka Chelekom Headless OTP Field ships zero opinionated styling - only the semantic markup, full WAI-ARIA wiring, and a roving-tabindex slot group backed by one logical value. Typing replaces and auto-advances, paste distributes and normalizes across the slots, Backspace clears (Ctrl/Meta+Backspace clears all), Delete removes-and-shifts, and Arrow/Home/End navigate between slots. The combined value mirrors into a hidden input for native form submission and validation. Every part - input, separator, and value - is addressable through a *_class attribute, so you style it entirely with your own Tailwind classes while the accessibility and behavior keep working underneath.

Base UI OTP Field example (Phoenix headless)

Base UI OTP Field example (Phoenix headless)

Enter the 6-character code we sent to your device.

heex
<div class="flex w-full max-w-80 flex-col items-start gap-1">
  <label
    for="otp-field-hero"
    class="text-sm font-bold text-neutral-950 dark:text-white"
  >
    Verification code
  </label>
  <.otp_field
    id="otp-field-hero"
    length={6}
    label="Verification code"
    class="flex w-full gap-2"
    input_class="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
  />
  <p class="m-0 text-sm text-neutral-600 dark:text-neutral-400">
    Enter the 6-character code we sent to your device.
  </p>
</div>

Alphanumeric OTP Field

Alphanumeric OTP Field

Accept letters and numbers for backup codes such as A7C9XZ.

heex
<div class="flex w-full max-w-80 flex-col items-start gap-1">
  <label
    for="otp-field-alphanumeric"
    class="text-sm font-bold text-neutral-950 dark:text-white"
  >
    Recovery code
  </label>
  <.otp_field
    id="otp-field-alphanumeric"
    length={6}
    validation_type="alphanumeric"
    label="Recovery code"
    class="flex w-full gap-2"
    input_class="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
  />
  <p class="m-0 text-sm text-neutral-600 dark:text-neutral-400">
    Accept letters and numbers for backup codes such as A7C9XZ.
  </p>
</div>

Masked OTP Field

Masked OTP Field

Use mask to obscure the code on shared screens.

heex
<div class="flex w-full max-w-80 flex-col items-start gap-1">
  <label
    for="otp-field-password"
    class="text-sm font-bold text-neutral-950 dark:text-white"
  >
    Access code
  </label>
  <.otp_field
    id="otp-field-password"
    length={6}
    mask
    label="Access code"
    class="flex w-full gap-2"
    input_class="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
  />
  <p class="m-0 text-sm text-neutral-600 dark:text-neutral-400">
    Use mask to obscure the code on shared screens.
  </p>
</div>

Focused Placeholder

Focused Placeholder

Placeholder hints can stay visible until the active slot is focused.

heex
<div class="flex w-full max-w-80 flex-col items-start gap-1">
  <label
    for="otp-field-focused-placeholder"
    class="text-sm font-bold text-neutral-950 dark:text-white"
  >
    Verification code
  </label>
  <.otp_field
    id="otp-field-focused-placeholder"
    length={6}
    placeholder="•"
    label="Verification code"
    class="flex w-full gap-2"
    input_class="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 placeholder:text-neutral-500 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white focus:placeholder:text-transparent dark:border-white dark:text-white dark:placeholder:text-neutral-400"
  />
  <p class="m-0 text-sm text-neutral-600 dark:text-neutral-400">
    Placeholder hints can stay visible until the active slot is focused.
  </p>
</div>

Grouped Slots

Grouped Slots

heex
<div class="flex w-full max-w-80 flex-col items-start gap-1">
  <label
    for="otp-field-grouped"
    class="text-sm font-bold text-neutral-950 dark:text-white"
  >
    Verification code
  </label>
  <.otp_field
    id="otp-field-grouped"
    length={6}
    group={3}
    separator=""
    label="Verification code"
    class="flex w-full items-center gap-2"
    input_class="m-0 h-10 w-10 rounded-none border border-neutral-950 bg-white dark:bg-neutral-950 text-center font-inherit text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
    separator_class="h-px w-4 bg-current text-neutral-950 dark:text-white"
  />
</div>

Parts & data attributes

Each part carries a stable data-part and a structural chelekom-otp_field__* class you can target, plus filled/complete/focused state for styling each slot.

Partdata-partState / ARIAPurpose
Rootrootrole="group", aria-label, data-disabled, data-readonly, data-required, data-complete, data-filled, data-focusedCarries the Otp hook; reads length/validation_type/transform/mask from data-*.
Inputinputdata-filled, data-complete, data-focusedOne single-char slot per character; roving tabindex (only the active slot is tabbable).
Separatorseparator-Optional divider between groups (group + separator).
Valuevalue-Hidden input carrying the combined value, plus pattern/required for form submission.

Accessibility

Follows the WAI-ARIA Authoring Practices Guide for segmented / OTP inputs: typing replaces and auto-advances, Backspace clears and moves back (Ctrl/Meta+Backspace clears every slot), Delete removes and shifts trailing characters, and Arrow Left/Right, Home/ArrowUp, and End/ArrowDown navigate between slots with a roving tabindex so only the active slot is reachable by Tab. Pasting a code distributes and normalizes it across the slots.