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 witharia-labelor a<label for>. - The input keeps its native
autocomplete="one-time-code"by default, so browsers and password managers can offer SMS autofill. inputmodeand apatternmatching thetypeoption 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 | component | Wrapper; holds the real input and the rendered slots |
otp-slots | part | Container for the rendered, decorative slots |
otp-slot | part | One character slot |
otp-separator | part | Divider shown between groups |
otp-connected | modifier | Merges the slots into one bar with shared borders and no gap |
otp-sm | size | Smaller slots |
otp-lg | size | Larger 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);
