Skip to content

One-Time Password Input ​

x-h-otp turns a native input into one or more groups of single-character cells for entering a verification code or PIN. The inner input keeps carrying the value, the field name, and any native constraints, so the code submits and validates as one string.

Usage ​

Wrap a native <input> in an element carrying x-h-otp, then add an x-h-otp-group for each run of cells. Every group declares its own data-length, and the full code is the groups read left to right. Put an x-h-otp-separator between two groups to show a visual break, with whatever text you want as its content.

The input stays in the markup but is visually hidden. It holds the joined value, the name submitted with the form, and constraints such as required. Bind x-model to it to read the code. The control matches the height of a standard Input and takes the same data-size on the root, which applies to every group.

Typing fills the cells left to right and advances automatically, including across group boundaries, and pasting a full code distributes it across all of them.

Keyboard Handling ​

  • 0-9 (and letters when alphanumeric) - Fill the focused cell and move to the next one.
  • Backspace - Clear the focused cell, or clear the previous cell and move back when the focused one is already empty.
  • Delete - Clear the focused cell without moving.
  • Left / Right - Move between cells, following the writing direction.
  • Home / End - Jump to the first or last cell.
  • Tab - Move out of the control, which is a single tab stop from the outside.

Pasting works from any cell and fills forward from it, so a partially entered code can be corrected without clearing it first.

Accessibility ​

The cells are grouped in a role="group" labelled "One-time password" by default. Set an aria-label or aria-labelledby on the element to give it a more meaningful name. Each cell gets its own label ("Digit 1 of 6") counting across every group, so screen readers announce the position in the whole code, and data-cell-label overrides the template. Separators are decorative and hidden from assistive technologies.

The inner input's disabled, readonly, required, and aria-invalid states are mirrored onto every cell, so the control announces and styles correctly without duplicating those attributes on the groups.

API Reference ​

Component attribute(s) ​

x-h-otp
x-h-otp-group
x-h-otp-separator

Attributes ​

x-h-otp ​

AttributeTypeRequiredDescription
data-typenumeric
alphanumeric
falseCharacters the cells accept. Defaults to numeric.
data-maskbooleanfalseHides the entered characters, for a PIN.
data-valuestringfalseInitial code. Falls back to the inner input's own value.
data-cell-labelstringfalseTemplate for each cell's accessible name. {index} and {length} are substituted. Defaults to Digit {index} of {length}.
data-sizesmfalseRenders smaller cells. Applies to every group.

x-h-otp-group ​

AttributeTypeRequiredDescription
data-lengthnumbertrueNumber of cells in this group.

Model ​

Bind a string with x-model on the inner input. The value is the concatenated code with no separators, and it updates on every character, paste, and clear. Setting it externally refills the cells, and characters outside the configured type are ignored.

Events ​

Events are dispatched on the inner input, except complete which is dispatched on the root.

EventDescription
inputFired on every change to the code.
changeFired alongside input when the user edits or pastes.
completeFired once when the last empty cell is filled. The full code is in event.detail.value.

Validation timing ​

By default this control shows native-constraint errors only after the user attempts to submit the form, since the inner input is never edited directly. To validate on load instead, set data-validate="immediate" on a wrapping x-h-fieldset, x-h-field, or any ancestor element. Setting aria-invalid="true" on the inner input always shows the error immediately. See Fieldset for details.

Data Slots ​

SlotElement
otpx-h-otp
otp-inputThe native input inside x-h-otp
otp-groupx-h-otp-group
otp-cellCharacter box created by x-h-otp-group
otp-separatorx-h-otp-separator

Examples ​

Basic ​

html
<div x-h-otp>
  <input name="code" />
  <div x-h-otp-group data-length="6"></div>
</div>

Grouped ​

html
<div x-h-otp>
  <input name="grouped" />
  <div x-h-otp-group data-length="3"></div>
  <div x-h-otp-separator>-</div>
  <div x-h-otp-group data-length="3"></div>
</div>

PIN ​

html
<div x-h-otp data-mask aria-label="PIN">
  <input name="pin" />
  <div x-h-otp-group data-length="4"></div>
</div>

Alphanumeric ​

html
<div x-h-otp data-type="alphanumeric">
  <input name="serial" />
  <div x-h-otp-group data-length="4"></div>
  <div x-h-otp-separator>-</div>
  <div x-h-otp-group data-length="4"></div>
</div>

Small ​

html
<div x-h-otp data-size="sm">
  <input name="small" />
  <div x-h-otp-group data-length="3"></div>
  <div x-h-otp-separator>-</div>
  <div x-h-otp-group data-length="3"></div>
</div>

Binding a value ​

html
<div x-data="{ code: 587231 }">
  <div x-h-otp>
    <input name="verification" x-model="code" />
    <div x-h-otp-group data-length="3"></div>
    <div x-h-otp-separator>-</div>
    <div x-h-otp-group data-length="3"></div>
  </div>
  <p class="mt-4 text-sm text-muted-foreground">Code: <span x-text="code || 'empty'"></span></p>
</div>

Reacting to completion ​

html
<div x-data="{ status: 'Waiting for the full code' }">
  <div x-h-otp @complete="status = 'Submitted ' + $event.detail.value">
    <input name="confirm" />
    <div x-h-otp-group data-length="4"></div>
  </div>
  <p class="mt-4 text-sm text-muted-foreground" x-text="status"></p>
</div>

Disabled ​

html
<div x-h-otp data-value="4821">
  <input name="disabled-code" disabled />
  <div x-h-otp-group data-length="3"></div>
  <div x-h-otp-separator>-</div>
  <div x-h-otp-group data-length="3"></div>
</div>

In a field ​

html
<div x-h-field>
  <label x-h-label>Verification code</label>
  <div x-h-otp>
    <input name="required-code" required minlength="6" />
    <div x-h-otp-group data-length="6"></div>
  </div>
  <p x-h-field-description>Enter the six-digit code we sent you.</p>
</div>