OTP Field
v0.0.10
mix mishka.ui.gen.headless otp_field
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.
<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.
<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.
<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.
<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
<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.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Root id; slots derive {id}-1, {id}-2, … (root keeps {id} for the hook) |
name
|
string
| — | — | No | Name for the hidden form input carrying the code |
value
|
string
| — | — | No | Initial/controlled OTP value |
length
|
integer
|
6
| — | No | Number of slots |
validation_type
|
string
|
numeric
| — | No | numeric | alpha | alphanumeric | none |
mask
|
boolean
|
false
| — | No | Obscure entered characters (-webkit-text-security; no password-manager hijack — Chromium/WebKit) |
transform
|
string
|
none
| — | No | Normalize accepted input: none | uppercase | lowercase |
input_mode
|
string
| — | — | No | Override the virtual-keyboard hint |
auto_complete
|
string
|
one-time-code
| — | No | autocomplete for the first slot + hidden input |
auto_submit
|
boolean
|
false
| — | No | Submit the owning form when the code completes |
disabled
|
boolean
|
false
| — | No | Disable interaction (data-disabled) |
readonly
|
boolean
|
false
| — | No | Block changes but allow navigation (data-readonly) |
required
|
boolean
|
false
| — | No | Mark required for form validation (data-required) |
placeholder
|
string
| — | — | No | Per-slot placeholder |
form
|
string
| — | — | No | Form id owning the hidden input |
label
|
string
|
One-time code
| — | No | Accessible name for the slot group |
group
|
integer
| — | — | No | Insert a separator every N slots (e.g. 3 ⇒ 123-456) |
separator
|
string
| — | — | No | Separator content rendered between groups |
on_change
|
string
| — | — | No | LiveView event pushed on change ({value}) |
on_complete
|
string
| — | — | No | LiveView event pushed when complete ({value}) |
on_invalid
|
string
| — | — | No | LiveView event pushed when characters are rejected ({value}) |
class
|
any
| — | — | No | Extra classes for the root |
input_class
|
any
| — | — | No | Extra classes for each slot input (data-part=input) |
separator_class
|
any
| — | — | No | Extra classes for the separator (data-part=separator) |
value_class
|
any
| — | — | No | Extra classes for the hidden form input (data-part=value) |
rest
|
global
| — | — | No | — |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
otp_field
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless otp_field