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)
Enter the 6-character code we sent to your device.
<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
Accept letters and numbers for backup codes such as A7C9XZ.
<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
Use mask to obscure the code on shared screens.
<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
Placeholder hints can stay visible until the active slot is focused.
<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
<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.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Root | root | role="group", aria-label, data-disabled, data-readonly, data-required, data-complete, data-filled, data-focused | Carries the Otp hook; reads length/validation_type/transform/mask from data-*. |
| Input | input | data-filled, data-complete, data-focused | One single-char slot per character; roving tabindex (only the active slot is tabbable). |
| Separator | separator | - | Optional divider between groups (group + separator). |
| Value | value | - | 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.