Menu

OTP input

A single input rendered as separate character slots, so a one-time password stays one real field for autofill, password managers and screen readers.

Overview

An OTP input looks like several boxes, but it is one real <input>. JavaScript renders one .otp-slot per character on top of it and turns the field itself into a transparent overlay, so screen readers, password managers and SMS autofill still see a single ordinary text field.

Add data-bs-toggle="otp" to a wrapper with a text input inside, and Tabler renders the slots for you.

<div class="otp" data-bs-toggle="otp" data-bs-length="6">
  <input type="text" aria-label="Verification code" />
</div>

Usage

Connected slots

Add .otp-connected to merge the slots into one bar with shared borders and no gap between them.

<div class="otp otp-connected" data-bs-toggle="otp" data-bs-length="6">
  <input type="text" aria-label="Verification code" />
</div>

Groups and a separator

Use data-bs-groups to split the slots into visual groups, for example [3,3] for a "123 456" layout. data-bs-separator sets the character shown between groups; it defaults to ·.

<div class="otp" data-bs-toggle="otp" data-bs-length="6" data-bs-groups="[3,3]">
  <input type="text" aria-label="Verification code" />
</div>

Sizes

Add .otp-sm or .otp-lg for a smaller or larger control.

<div class="otp otp-sm" data-bs-toggle="otp" data-bs-length="4">
  <input type="text" aria-label="PIN" />
</div>
<div class="otp otp-lg" data-bs-toggle="otp" data-bs-length="4">
  <input type="text" aria-label="PIN" />
</div>

Alphanumeric and masked

Set data-bs-type to alphanumeric or alpha to accept letters as well as digits. Add data-bs-mask="true" to show a mask character instead of the typed value - getValue() still returns the real value.

<div class="otp" data-bs-toggle="otp" data-bs-length="6" data-bs-type="alphanumeric" data-bs-mask="true">
  <input type="text" aria-label="Recovery code" />
</div>

Disabled

Add disabled to the inner input.

<div class="otp" data-bs-toggle="otp" data-bs-length="6">
  <input type="text" value="123456" disabled aria-label="Verification code" />
</div>

Validation

Add .is-valid or .is-invalid to the .otp wrapper, the same way you would on a .form-control.

<div class="otp is-invalid" data-bs-toggle="otp" data-bs-length="6">
  <input type="text" aria-label="Verification code" />
</div>

Options

Options are read from data-bs-* attributes on the .otp element (the data-tblr-* alias also works), or passed to the constructor.

Option Type Default Description
length number null Number of slots. When null, it is read from the input's maxlength, or 6.
type string 'numeric' numeric, alphanumeric, or alpha - which characters each slot accepts.
mask boolean false Shows a mask character in filled slots instead of the real value.
groups array or null null Slot counts per visual group, for example [3, 3].
separator string '·' Character shown between groups.

JavaScript

Tabler creates an OtpInput for every element matching [data-bs-toggle="otp"] (or [data-tblr-toggle="otp"]):

initAll(SELECTOR_DATA_TOGGLE, OtpInput)

Create one manually, or get an existing instance:

const el = document.querySelector('.otp')
const otp = tabler.OtpInput.getOrCreateInstance(el, { length: 6, type: 'numeric' })

Listen for complete.bs.otpInput to know when every slot is filled:

el.addEventListener('complete.bs.otpInput', (event) => {
  console.log(event.value)
})

Methods

Method Description
getValue() Returns the current value.
setValue(value) Sets the value and re-renders the slots.
clear() Empties the value and focuses the input.
focus() Focuses the input and selects the first empty slot.
dispose() Removes the rendered slots and the instance.
getInstance(element) Static. Returns the instance bound to the element, or null.
getOrCreateInstance(element, config) Static. Returns the existing instance, or creates one.

Events

Event Description
input.bs.otpInput Fires on every change from typing, backspace, paste, or autofill. event.value holds the current value.
complete.bs.otpInput Fires once the value fills every slot, including from setValue(). event.value holds the full value.

Accessibility

  • The visible slots are decoration (aria-hidden); a screen reader only ever sees the one real input, so label it like any other field with aria-label or a <label for>.
  • The input keeps its native autocomplete="one-time-code" by default, so browsers and password managers can offer SMS autofill.
  • inputmode and a pattern matching the type option are set automatically, so mobile keyboards show the right layout.
  • The mask option only hides the value visually - the input is never switched to type="password", so assistive tech still reads it as plain text.
  • Keyboard focus lands on the first empty slot, and typing overwrites the active slot rather than inserting, so screen reader and switch-access users always land where they expect.

Class reference

Every class this component ships, grouped by what it changes. A name in braces stands for a family — {color} is any base color, {breakpoint} any responsive step.

otp componentWrapper; holds the real input and the rendered slots
otp-slots partContainer for the rendered, decorative slots
otp-slot partOne character slot
otp-separator partDivider shown between groups
otp-connected modifierMerges the slots into one bar with shared borders and no gap
otp-sm sizeSmaller slots
otp-lg sizeLarger slots

CSS

Variables

The component uses local CSS variables on .otp for real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. Source: core/scss/ui/forms/_form-otp.scss.

--otp-size: #{$otp-size};
--otp-font-size: #{$otp-font-size};
--otp-gap: #{$otp-gap};
--otp-slot-color: #{$otp-slot-color};
--otp-slot-bg: #{$otp-slot-bg};
--otp-slot-border-width: #{$otp-slot-border-width};
--otp-slot-border-color: #{$otp-slot-border-color};
--otp-slot-border-radius: #{$otp-slot-border-radius};
--otp-slot-box-shadow: #{$otp-slot-box-shadow};
--otp-separator-color: #{$otp-separator-color};

Sass variables

Override these variables before importing Tabler to change the defaults. Source: core/scss/_variables.scss.

$otp-size: $input-height;
$otp-font-size: $input-font-size;
$otp-gap: 0.5rem;
$otp-slot-color: var(--body-color);
$otp-slot-bg: $input-bg;
$otp-slot-border-width: $input-border-width;
$otp-slot-border-color: var(--border-color);
$otp-slot-border-radius: $input-border-radius;
$otp-slot-box-shadow: var(--shadow-input);
$otp-separator-color: var(--secondary);