OTP / PIN Input

Configurable verification codes and PINs with Material slots, native forms, and Maskito patterns.

Basic example

A Material input for verification codes and PINs. Choose the number of characters, type or paste a code, and edit any slot. One native input preserves keyboard selection, labels, form values, and browser one-time-code autofill support.

Enter all six digits. Paste is supported; leading zeroes are preserved.

Loading OTP input…

Submit a complete code to inspect the native form value.
<div class="input-field outlined">
  <input id="verification-code" name="code" type="text" placeholder=" "
         data-otp data-otp-length="6" autocomplete="one-time-code"
         aria-describedby="verification-help" required>
  <label for="verification-code">Verification code</label>
  <span id="verification-help" class="supporting-text">Enter all six digits.</span>
</div>

Use the same input-field outlined wrapper as other text inputs. OTP slots share the 4px corner radius and 1px outline, with a 2px active border. The label stays above the group so it remains readable across the individual slots.

import { OtpInput } from 'kmaterialize';
const field = OtpInput.init(input, {
  length: 6,
  characters: 'digits',
  onComplete: () => { status.textContent = 'Code complete. Ready to submit.'; }
});
await field.ready;

Completion means every slot is filled; verification happens in your application. The component does not submit the form automatically.

Setup

Start with the kmaterialize CSS and JavaScript from Getting started, then follow the component setup below.

npm install kmaterialize @maskito/core
<div class="input-field outlined">
  <input id="verification" name="code" type="text" data-otp
         data-otp-length="6" autocomplete="one-time-code" required>
  <label for="verification">Verification code</label>
</div>
import 'kmaterialize/dist/css/materialize.css';
import { AutoInit, OtpInput } from 'kmaterialize';
AutoInit();
const field = OtpInput.getInstance(input);
await field.ready;
await field.setValue('012345'); // Keeps the leading zero; emits input.
field.isComplete();           // true
await field.clear();
await field.refresh();       // Sync after assigning input.value directly.
field.focus();
field.destroy();              // Restores the native input and attributes.

Use no-autoinit for explicit initialization. Await ready before calling the API. Without a bundler, expose Maskito core exports as window.MaskitoCore before initializing M.OtpInput. The plain native input remains available if the optional library cannot load.

Examples and variants

Bottom-line variant

Omit outlined for the same bottom-line treatment as a standard text input. Each slot has a 1px underline, increasing to 2px on the active slot. Length, masks, paste, and validation work the same way.

Enter or paste your six-digit code.
<div class="input-field">
  <input id="verification" name="code" type="text" placeholder=" "
         data-otp data-otp-length="6" autocomplete="one-time-code"
         aria-describedby="verification-support">
  <label for="verification">Verification code</label>
  <span id="verification-support" class="supporting-text">Enter your six-digit code.</span>
</div>

Use M.AutoInit(), or initialize explicitly with OtpInput.init(input). Add outlined to the wrapper to switch back to boxed slots.

Hidden PIN

Characters appear as dots. Use the button to reveal or hide them.
<div class="input-field outlined">
  <input id="pin" type="password" data-otp data-otp-length="4"
         autocomplete="off" placeholder=" ">
  <label for="pin">Four-digit PIN</label>
</div>

Grouped digits

groupSize: 3 adds space after each group of three. The value remains six digits, without a separator.

OtpInput.init(input, { length: 6, groupSize: 3 });

Custom Maskito pattern

Try pasting fx2048. Letters become uppercase and the hyphen is inserted automatically.

Pattern: AA-####

const license = OtpInput.init(input, {
  pattern: 'AA-####',
  maskOptions: {
    overwriteMode: 'replace',
    postprocessors: [({ value, selection }) => ({
      value: value.toUpperCase(), selection
    })]
  }
});
await license.ready;
await license.setValue('fx2048');
license.getValue();         // "FX-2048" — also the native form value
license.getUnmaskedValue(); // "FX2048"

The pattern determines the number of editable slots: # is a digit, A an ASCII letter, and * an ASCII letter or digit. Other characters are fixed separators; a backslash escapes a token. If you also supply length, it must match the pattern's editable slots.

Pass Maskito processors, plugins, and overwrite options through maskOptions. The component owns the fixed mask so the slots and value stay aligned. Processors should preserve the pattern's positions. See more examples on the Input Masks page.

Native field states

Use native required, disabled, readonly, and aria-describedby. Incomplete nonempty values fail pattern validation. For a server-rejected code, set aria-invalid="true" and describe the error beside the field. Clear that attribute when the error is resolved.

Validation

Use required for an empty code. OtpInput validates incomplete values against the slot pattern. A server-rejected code uses aria-invalid="true" with a clear message; clear it when the user corrects the code.

These previews keep their error state visible. In your form, show errors after checking a field or submitting, then clear them when the value is valid.

For text fields, use .input-field.error, aria-invalid="true", and a message linked with aria-describedby. Remove the error state when resolved. Use aria-live="polite" for messages updated dynamically.

Required
Enter the six-digit verification code.
Incomplete code
Enter all six digits.
<div class="input-field outlined error">
    <input id="validation-otp" type="text" data-otp data-otp-length="6" data-otp-align="left" required autocomplete="one-time-code" placeholder=" " value="" aria-invalid="true" aria-describedby="validation-otp-error">
    <label for="validation-otp">Verification code</label>
    <span id="validation-otp-error" class="supporting-text"><i class="material-icons" aria-hidden="true">error</i> Enter the six-digit verification code.</span>
</div>

<div class="input-field error">
    <input id="validation-otp-incomplete" type="text" data-otp data-otp-length="6" data-otp-align="left" required placeholder=" " value="123" aria-invalid="true" aria-describedby="validation-otp-incomplete-error">
    <label for="validation-otp-incomplete">Verification code</label>
    <span id="validation-otp-incomplete-error" class="supporting-text"><i class="material-icons" aria-hidden="true">error</i> Enter all six digits.</span>
</div>

Reference

Options and methods

OptionBehavior
length1–32 editable slots; defaults to 6 without a pattern.
charactersdigits (default) or alphanumeric.
patternA fixed Maskito shorthand pattern, such as ###-### or AA-####.
groupSizeVisual grouping only; 0 disables it.
maskedOptional boolean; otherwise preserves the original input type.
maskOptionsMaskito processing, plugins, and overwrite mode. Default mode is replace.
onCompleteCalled with the formatted value and instance when an edit produces a new complete code.

The bubbling otpcomplete event exposes detail.value and detail.unmaskedValue. Initialization, reset, refresh, and setValue(value, false) do not emit completion events. Standard input/change events and FormData use the original input.

Markup options: data-otp-length, data-otp-characters, data-otp-pattern, data-otp-group-size, and data-otp-masked. JavaScript options take precedence.

Customize --otp-slot-size, --otp-slot-gap, and --otp-slot-radius on .otp-input. Colors follow the current Material theme. Slots wrap on narrow screens; codes remain ordered left to right in RTL pages.