npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

otp-input-kit

v1.1.0

Published

Highly customizable, framework-agnostic OTP input component with RTL, i18n, a11y, timer, and Web Component support

Readme

otp-input-kit

otp-input-kit preview

A highly customizable, framework-agnostic OTP input component with full RTL support, i18n, accessibility, countdown timer, toast notifications, and Web Component support — zero dependencies.

npm version license


Features

  • Zero dependencies — pure vanilla JS, ~17 KB gzipped (CSS inlined)
  • 10 built-in themes — default, underline, rounded, pill, ghost, filled, soft, neon, gradient, elevated
  • Full RTL & i18n — 12+ RTL locales, 8 numeral systems (Arabic-Indic, Persian, Hindi, Bengali, Tamil, Thai…)
  • WCAG 2.1 AA accessible — ARIA labels, live error regions, keyboard navigation, high-contrast & reduced-motion support
  • Countdown timer — progress-bar or circular ring style, with expiry callback
  • Audio feedback — optional Web Audio key/success/error tones (no asset files)
  • Resend button with configurable cooldown
  • Toast notifications — 6 themes, 9 positions, auto-dismiss
  • Clipboard paste detection with smart OTP extraction
  • Undo/Redo (Ctrl+Z / Ctrl+Shift+Z)
  • Haptic feedback (mobile vibration)
  • Async verification — built-in loading spinner that awaits your server and resolves to a success or error state
  • Secure mode — password masking with optional 👁 reveal toggle and brute-force attempt lockout
  • On-screen keypad — optional built-in virtual number pad (mobile / kiosk / PIN), with shuffle-to-obscure
  • Success celebration — success animation styles (pop / glow / bounce / flip) + optional 🎉 confetti burst
  • Web Component <otp-input> — drop in anywhere
  • Framework adapters — first-class React, Vue 3, Svelte, and Angular wrappers
  • TypeScript — ships full type declarations for the core, Web Component, and every adapter
  • Form-associated<otp-input name="otp"> submits, validates, and resets natively inside a <form>
  • ESM + UMD + CJS builds for every environment

Installation

npm

npm install otp-input-kit

CDN (no build step)

<script src="https://unpkg.com/otp-input-kit/dist/otp-input.umd.min.js"></script>

Quick Start

ES Module (bundler / Vite / Webpack)

import OTPInput from 'otp-input-kit';

const otp = OTPInput.create('#container', {
  length: 6,
  onComplete: (value) => console.log('OTP:', value),
});

UMD via <script> tag

<div id="container"></div>
<script src="https://unpkg.com/otp-input-kit/dist/otp-input.umd.min.js"></script>
<script>
  OTPInput.create('#container', {
    length: 6,
    onComplete: (value) => verifyOTP(value),
  });
</script>

Web Component

<script type="module">
  import 'otp-input-kit';
</script>

<otp-input length="6" theme="rounded" direction="rtl" locale="ar" native-numerals></otp-input>

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | length | number | 6 | Number of OTP digits | | type | 'numeric' \| 'alpha' \| 'alphanumeric' \| 'hex' \| 'custom' | 'numeric' | Allowed character type | | pattern | RegExp | null | Custom pattern (requires type: 'custom') | | secure | boolean | false | Mask input like a password field | | revealToggle | boolean | false | Add an 👁 button to peek at masked digits (secure mode) | | lockout | object | see below | Lock input after too many failed attempts | | autoFocus | boolean | true | Focus first input on init | | autoSubmit | boolean | false | Submit parent <form> on completion | | selectOnFocus | boolean | true | Select digit text on focus | | direction | 'ltr' \| 'rtl' \| 'auto' | 'auto' | Input reading direction | | locale | string | null | BCP 47 locale tag (e.g. 'ar', 'fa', 'he') | | nativeNumerals | boolean | false | Render locale-specific digit glyphs | | placeholder | string | '·' | Empty cell placeholder character | | clipboardDetection | boolean | true | Detect and auto-fill pasted OTPs | | haptic | boolean | true | Vibration feedback on mobile | | theme | string | 'default' | Input theme (see Themes) | | validate | Function | null | (value) => errorString \| null | | animation | object | see below | Error/success animation + confetti config | | keypad | boolean \| object | false | On-screen virtual number pad (see below) | | timer | object | see below | Countdown timer config | | resend | object | see below | Resend button config | | toast | object | see below | Toast notification config | | onVerify | Function | null | async (value) => result — drives the loading→success/error flow (see Async Verification) | | loading | object | see below | Loading/verifying state config |

animation

animation: {
  // 'shake' | 'highlight' | 'both' | 'pulse' | 'buzz' | 'bounce' | 'glow' | 'wobble' | false
  error: 'shake',
  // true | false | 'pop' | 'glow' | 'bounce' | 'flip'
  success: true,
  confetti: false,  // 🎉 burst on success
  duration: 300,    // ms
}

All error styles animate every cell (errors normally fire when the code is complete), respect prefers-reduced-motion, and trigger a haptic buzz on mobile.

keypad

An optional on-screen number pad — great for mobile, kiosks, and PIN entry.

keypad: {
  enabled: false,
  randomize: false,      // shuffle key order (anti shoulder-surfing for PINs)
  showClear: false,      // include a "clear all" key
  backspaceLabel: '⌫',
  clearLabel: 'Clear',
}
// shorthand: keypad: true

timer

timer: {
  enabled: true,
  duration: 60,          // seconds
  showProgress: true,    // animated progress bar
  style: 'bar',          // 'bar' | 'ring' (circular countdown)
  onExpire: () => {},    // callback when time runs out
}

sound

Subtle Web Audio feedback (keypress / success / error) — no asset files, created lazily on first key, off by default.

sound: {
  enabled: false,
  volume: 0.2,           // 0..1
}

resend

resend: {
  enabled: true,
  cooldown: 60,                // seconds between resends
  label: 'Resend code',
  onResend: () => {},          // called when user clicks resend
}

toast

toast: {
  enabled: true,
  position: 'top-right',      // see positions below
  theme: 'default',           // see themes below
  duration: 3500,             // auto-dismiss delay in ms
  successMessage: 'Verified!',
  errorMessage: 'Invalid code',
}

loading

loading: {
  text: 'Verifying…',                   // a11y label shown while verifying
  successText: 'Verified',
  errorText: 'Verification failed',     // shown when onVerify rejects/returns false
  clearOnError: true,                   // wipe the inputs after a failed verify
  clearDelay: 900,                      // ms to keep the error visible before clearing
}

lockout

lockout: {
  enabled: false,
  maxAttempts: 3,        // failed tries before locking
  duration: 30,          // seconds the input stays locked
  message: 'Too many attempts. Try again in {seconds}s.', // {seconds} counts down
  onLock:   (secondsRemaining) => {},
  onUnlock: () => {},
}

A failed attempt is counted on a rejected onVerify, a completion-validation failure, or a manual setError(). The counter resets on success or unlock.


Callbacks

OTPInput.create('#container', {
  onChange:   (value) => {},              // fires on every keystroke
  onComplete: (value) => {},             // fires when all digits filled
  onError:    (errors) => {},            // fires on validation failure
  onFocus:    ({ index, input }) => {},  // fires when a cell is focused
  onBlur:     ({ index, input }) => {},  // fires when a cell loses focus
  onExpire:   () => {},                  // fires when timer expires
  onResend:   () => {},                  // fires when resend is clicked
  onVerified: (value) => {},             // fires when async verify succeeds
  onFailed:   (message) => {},           // fires when async verify fails
});

Methods

const otp = OTPInput.create('#container', options);

otp.getValue()           // → string of current digits
otp.setValue('123456')   // fill all cells programmatically
otp.clear()              // clear all cells
otp.focus()              // focus first empty cell
otp.setError('msg')      // show error state with message
otp.clearError()         // remove error state
otp.setLoading(true)     // toggle the verifying spinner + disable inputs
otp.setSuccess('msg')    // manually enter the verified/success state
otp.toggleReveal()       // peek at masked digits (secure mode); pass a boolean to force
otp.lock()               // lock the input for the cooldown
otp.unlock()             // release a lock early and reset the attempt counter
otp.isLocked()           // → boolean
otp.destroy()            // unmount and clean up DOM
otp.startTimer()         // start / restart countdown
otp.stopTimer()          // stop countdown

Events (EventEmitter API)

otp.on('complete',      (value) => {});
otp.on('change',        (value) => {});
otp.on('error',         (errors) => {});
otp.on('focus',         ({ index }) => {});
otp.on('blur',          ({ index }) => {});
otp.on('expire',        () => {});
otp.on('resend',        () => {});
otp.on('verify-start',  (value) => {});   // async verification began
otp.on('verified',      (value) => {});   // async verification succeeded
otp.on('verify-failed', (message) => {}); // async verification failed
otp.on('sms-read',      (code) => {});    // Web OTP API auto-filled from SMS
otp.on('sms-unsupported', (reason) => {}); // 'no-api' | 'insecure-context'
otp.on('attempt', ({ attempts, max }) => {}); // a failed attempt was counted
otp.on('lock',    (secondsRemaining) => {});  // input locked out
otp.on('unlock',  () => {});                   // lock released

otp.off('complete', handler);
otp.once('complete', handler);

Web Component Attributes

<otp-input
  length="6"
  type="numeric"
  direction="rtl"
  locale="ar"
  native-numerals
  secure
  auto-focus
  auto-submit
  haptic
  placeholder="·"
  theme="rounded"
  timer-duration="60"
  resend-enabled
  resend-cooldown="60"
  clipboard-detection
  reveal-toggle
  lockout-attempts="3"
  lockout-duration="30"
  keypad
  keypad-randomize
  confetti
  success-animation="bounce"
  timer-style="ring"
  sound
  sound-volume="0.2"
  toast-enabled
  toast-theme="glass"
  toast-position="top-right"
  label="Enter verification code"
></otp-input>

Web Component Events

const el = document.querySelector('otp-input');
el.addEventListener('otp-complete',     (e) => console.log(e.detail));
el.addEventListener('otp-change',       (e) => console.log(e.detail));
el.addEventListener('otp-error',        (e) => console.log(e.detail));
el.addEventListener('otp-expire',       () => {});
el.addEventListener('otp-resend',       () => {});
el.addEventListener('otp-verify-start', (e) => {});
el.addEventListener('otp-verified',     (e) => console.log(e.detail));
el.addEventListener('otp-failed',       (e) => console.log(e.detail));
el.addEventListener('otp-lock',         (e) => console.log(e.detail));
el.addEventListener('otp-unlock',       () => {});
el.addEventListener('otp-attempt',      (e) => console.log(e.detail));

Because functions can't be HTML attributes, attach onVerify as a property:

el.onVerify = async (code) => (await fetch(`/verify/${code}`)).ok;

Form integration

<otp-input> is a form-associated custom element, so it behaves like a native control inside a <form>:

<form id="login">
  <otp-input name="otp" length="6" required></otp-input>
  <button type="submit">Verify</button>
</form>

<script type="module">
  document.getElementById('login').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(e.target);
    console.log(data.get('otp')); // → the entered code
  });
</script>
  • The value is submitted under the element's name.
  • required makes the form invalid until the code is complete (:invalid, checkValidity(), reportValidity() all work).
  • Form reset clears it; browser autofill restore repopulates it.

Framework Adapters

The core is framework-agnostic, but first-class wrappers ship in the box.

React

import { OtpInput } from 'otp-input-kit/react';

function Login() {
  const ref = useRef(null);
  return (
    <OtpInput
      ref={ref}
      length={6}
      theme="rounded"
      onVerify={async (code) => (await api.verify(code)).ok}
      onVerified={() => navigate('/home')}
      onFailed={(msg) => toast.error(msg)}
    />
  );
}

// ref exposes: getValue, setValue, clear, focus, setError,
// setLoading, setSuccess, setTheme, resetTimer, getInstance

There's also a lightweight hook:

import { useOtp } from 'otp-input-kit/react';

const [ref, otp] = useOtp({ length: 6, onComplete: console.log });
return <div ref={ref} />;   // otp.current is the OTPInput instance

Vue 3

<script setup>
import { ref } from 'vue';
import { OtpInput } from 'otp-input-kit/vue';

const otp = ref(null);
const verify = async (code) => (await api.verify(code)).ok;
</script>

<template>
  <OtpInput
    ref="otp"
    :length="6"
    theme="pill"
    :on-verify="verify"
    @complete="onComplete"
    @verified="goHome"
    @failed="showError"
  />
</template>

Svelte

<script>
  import { otp } from 'otp-input-kit/svelte';
  const options = {
    length: 6,
    onVerify: async (code) => (await fetch(`/verify/${code}`)).ok,
  };
</script>

<div
  use:otp={options}
  on:complete={(e) => console.log(e.detail)}
  on:verified={goHome}
  on:failed={(e) => toast(e.detail)}
/>

Angular

A standalone directive ships as TypeScript source (Angular always compiles TS):

import { Component } from '@angular/core';
import { OtpInputDirective } from 'otp-input-kit/angular';

@Component({
  standalone: true,
  imports: [OtpInputDirective],
  template: `
    <div otpInput
         [length]="6"
         theme="rounded"
         [onVerify]="verify"
         (verified)="goHome()"
         (failed)="showError($event)"
         #otp="otpInput"></div>
  `,
})
export class LoginComponent {
  verify = async (code: string) => (await this.api.verify(code)).ok;
}

Prefer zero setup? The <otp-input> Web Component works in Angular out of the box — just add CUSTOM_ELEMENTS_SCHEMA to your component/module.

react, vue, and @angular/core are optional peer dependencies — install whichever you use. The Svelte adapter is a plain action with no dependency.


Themes

Pass via the theme option or the theme attribute on <otp-input>.

| Value | Preview description | |-------|-------------------| | default | Bordered boxes | | underline | Bottom border only | | rounded | Softly rounded corners | | pill | Fully rounded pill shape | | ghost | Transparent background | | filled | Solid fill background | | soft | Pastel/muted fill | | neon | Glowing accent border | | gradient | Gradient border | | elevated | Drop-shadow depth |

CSS Custom Properties

#my-container {
  --otp-input-width:    52px;
  --otp-input-height:   60px;
  --otp-font-size:      1.5rem;
  --otp-gap:            10px;
  --otp-radius:         10px;
  --otp-border-color:   #e2e8f0;
  --otp-active-color:   #3b82f6;
  --otp-error-color:    #ef4444;
  --otp-success-color:  #22c55e;
  --otp-bg:             #ffffff;
  --otp-text:           #0f172a;
}

RTL & Locale Examples

// Arabic — right-to-left with Eastern Arabic numerals
OTPInput.create('#ar', {
  length: 6, direction: 'rtl',
  locale: 'ar', nativeNumerals: true,
});

// Persian / Farsi
OTPInput.create('#fa', {
  length: 6, direction: 'rtl',
  locale: 'fa', nativeNumerals: true,
});

// Hebrew — RTL, Western numerals
OTPInput.create('#he', {
  length: 6, direction: 'rtl', locale: 'he',
});

Supported numeral systems: ar (Arabic-Indic ٠١٢٣٤٥٦٧٨٩), fa (Persian ۰۱۲۳۴۵۶۷۸۹), hi (Hindi ०१२३४५६७८९), bn (Bengali ০১২৩৪৫৬৭৮৯), ta (Tamil ௦௧௨௩௪௫௬௭௮௯), th (Thai ๐๑๒๓๔๕๖๗๘๙).


Validation

OTPInput.create('#container', {
  validate: (value) => {
    if (value === '000000') return 'This code is not allowed';
    if (!/^\d+$/.test(value)) return 'Numeric only';
    return null; // valid
  },
  onError: (errors) => console.error(errors),
});

Async Verification

Pass an onVerify function and the component takes over the full verify lifecycle: when the code completes it shows a loading spinner (and disables the inputs), awaits your function, then resolves to a success or error state automatically.

OTPInput.create('#container', {
  length: 6,
  onVerify: async (code) => {
    const res = await fetch('/api/verify', {
      method: 'POST',
      body: JSON.stringify({ code }),
    });
    return res.ok;                 // see "Return values" below
  },
  onVerified: (code) => location.assign('/dashboard'),
  onFailed:   (msg)  => console.warn('Rejected:', msg),
});

Return values

onVerify may return (or resolve to) any of:

| Return | Result | |--------|--------| | true / undefined | ✅ success — verified event | | false | ❌ failure with the default message | | 'Some message' | ❌ failure showing that message | | { ok: true } | ✅ success | | { ok: false, message } | ❌ failure with a custom message | | throws | ❌ failure using the thrown error's message |

On failure the inputs shake, show the error state, and (by default) clear after loading.clearDelay ms so the user can retry. Tune via the loading option.

Manual control

Don't want the automatic flow? Drive it yourself:

const otp = OTPInput.create('#container', {
  onComplete: async (code) => {
    otp.setLoading(true);
    try {
      await api.verify(code);
      otp.setSuccess('Welcome back!');
    } catch (e) {
      otp.setError('That code is incorrect');
    }
  },
});

Secure Mode & Lockout

OTPInput.create('#container', {
  length: 4,
  secure: true,         // mask digits like a password
  revealToggle: true,   // adds an 👁 button to peek
  onVerify: (code) => code === '4242',
  lockout: {
    enabled: true,
    maxAttempts: 3,     // after 3 wrong codes…
    duration: 30,       // …lock for 30s with a live countdown
    onLock:   () => analytics.track('otp_locked'),
    onUnlock: () => {},
  },
});

// Programmatic control
otp.toggleReveal(true); // force-show the digits
otp.lock();             // lock immediately
otp.unlock();           // release early

Timer + Resend

OTPInput.create('#container', {
  length: 6,
  timer: {
    enabled: true,
    duration: 60,
    showProgress: true,
    onExpire: () => console.log('Code expired'),
  },
  resend: {
    enabled: true,
    cooldown: 60,
    label: 'Resend code',
    onResend: () => sendNewCode(),
  },
});

Package Formats

| Format | File | Use case | |--------|------|----------| | ESM | dist/otp-input.esm.js | Vite, Webpack, Rollup | | UMD | dist/otp-input.umd.js | <script> tag | | CJS | dist/otp-input.umd.cjs | Node.js require() | | Minified UMD | dist/otp-input.umd.min.js | CDN / production |


Browser Support

Chrome 80+, Firefox 75+, Safari 14+, Edge 80+. No IE11 support.


License

MIT © Fady Ehab