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

input-otp

v1.5.0

Published

One-time password input component for React.

Readme

input-otp

One invisible input, any UI you can imagine.

The accessible, unstyled, fully featured one-time-password component for React.

npm downloads bundle size license

Documentation · Examples · API · Edge cases

Why

HTML has no one-time-password control. There is no <input type="otp">, so most products build one out of six separate inputs wired together with keydown handlers that shuffle focus between them — and quietly lose SMS autofill, screen reader support, partial paste, undo, and half the keyboard along the way.

input-otp renders exactly one real text input, paints it invisible, and hands you the state to draw whatever you want on top. Everything the browser gives a text field keeps working, because there is still a text field.

  • SMS autofillautocomplete="one-time-code" only means something on a single field
  • Screen readers — one control, one name, one value, one caret, one tab stop
  • Every keybinding you didn't implement — select-all, word-delete, shift-arrow ranges, undo, the iOS long-press menu
  • Real paste — including a partial paste into the middle of a half-filled code
  • Form semantics — one name, one entry in FormData, a real <label> that focuses it
  • Unstyled — no theme, no class names to override, no CSS to import
  • Small — zero dependencies, React 16.8 → 19 (see the size badge above)

Install

npm install input-otp

Usage

maxLength is the number of slots. render receives them and returns your markup — that's the whole contract.

'use client'
import { OTPInput } from 'input-otp'

export function VerificationCode() {
  return (
    <OTPInput
      maxLength={6}
      containerClassName="group flex items-center"
      render={({ slots }) => (
        <div className="flex">
          {slots.map((slot, idx) => (
            <Slot key={idx} {...slot} />
          ))}
        </div>
      )}
    />
  )
}

Each slot tells you what to draw:

import type { SlotProps } from 'input-otp'

function Slot({ char, placeholderChar, isActive, hasFakeCaret }: SlotProps) {
  return (
    <div
      className={cn(
        'relative flex h-14 w-12 items-center justify-center',
        'border-y border-r border-border first:rounded-l-md first:border-l last:rounded-r-md',
        'text-[1.375rem] font-medium tabular-nums transition-all duration-200',
        'outline outline-0 outline-foreground/80',
        isActive && 'z-10 outline-2', // this slot is being edited
      )}
    >
      {char ?? placeholderChar}
      {hasFakeCaret && <FakeCaret />} {/* the real caret is transparent */}
    </div>
  )
}

The full, copy-pasteable slot component (with the caret keyframe and the Stripe-style dash) is in Installation.

Using shadcn/ui?

shadcn/ui's input-otp component wraps this library with pre-composed parts. Same engine, <InputOTPSlot index={0} /> instead of a render prop:

npx shadcn@latest add input-otp

What it handles for you

The API is five props. The value is the list of things that go wrong when one invisible input has to behave like six boxes — and the fix for each:

| | | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | A collapsed caret has no slot | The selection is rewritten into a one-character range on every selectionchange — except at the append position, where a bare caret is meaningful | | ArrowLeft appears to skip a slot | Direction is inferred from the previous selection, with a guard for leaving insert mode | | Deleting doesn't fire selectionchange | The event is dispatched by hand when the value shrinks | | Password manager badges cover the last slot | A badge is detected by known extension markers, then by probing the field's top-right corner; the input widens 40px behind a clip-path — no visible layout shift | | iOS won't paste into an invisible input | The field keeps opacity: 1 and hides itself with transparent colours; paste is handled manually | | Autofill paints its own background | :autofill is neutralised, and the state is shaken off with a synthetic input event | | No JavaScript means no visible field | A <noscript> stylesheet turns the input back into a plain visible one |

Each of these — and a dozen more — is written up with the reasoning and the exact code in Edge cases.

Documentation

| | | | ---------------------------------------------------------------------- | --------------------------------------------------------------- | | Introduction | Why one input, and what you write | | Installation | Install, first render, a slot component to copy | | Anatomy | X-ray the field and watch the selection algorithm run live | | Styling | Slots, carets, placeholders, groups, data attributes | | Validation | pattern, pasteTransformer, inputMode | | Forms | Controlled values, auto-submit, react-hook-form, server actions | | Accessibility | Labelling, keyboard, what a screen reader hears | | Password managers | How badge detection works — with a live simulator | | Mobile & platforms | SMS autofill, iOS quirks, autofill styling, no-JS | | API reference | Every prop, render prop, data attribute and export | | Examples | A gallery of finished fields to copy | | Troubleshooting | The questions that come up most |

API at a glance

type OTPInputProps = {
  maxLength: number                       // number of slots — required

  render?: (props: RenderProps) => React.ReactNode
  children?: React.ReactNode              // …or compose and read OTPInputContext

  value?: string
  onChange?: (newValue: string) => unknown   // a string, not an event
  onComplete?: (...args: any[]) => unknown // fires once, on the transition to full;
                                           // receives the value as a string (narrows in 2.0)

  pattern?: string | RegExp               // gates every change; no default
  placeholder?: string                    // per-slot placeholder characters
  pasteTransformer?: (pasted: string) => string

  containerClassName?: string             // the visible wrapper
  // className goes to the invisible input

  textAlign?: 'left' | 'center' | 'right'          // default 'left'
  inputMode?: 'numeric' | 'text' | ...             // default 'numeric'
  pushPasswordManagerStrategy?: 'increase-width' | 'none'
  noScriptCSSFallback?: string | null
  nonce?: string                          // for CSP style-src — applied to the injected <style> tag
}

interface SlotProps {
  char: string | null
  placeholderChar: string | null
  isActive: boolean
  hasFakeCaret: boolean
}

Every other <input> attribute is forwarded — name, required, disabled, autoFocus, aria-*, data-* — and ref points at the real input. spellCheck defaults to false (browsers would underline a full code as a typo); pass spellCheck yourself to override.

Full reference: input-otp.rodz.dev/docs/api.

Contributing

pnpm install
pnpm build:lib          # tsup → packages/input-otp/dist
pnpm dev:playground     # the Playwright target, port 3039
pnpm test               # Playwright, all browsers

Tests live in apps/playground/src/tests. Note that the iOS code path, SMS autofill and password manager badges cannot be covered headlessly — see Mobile & platforms.

Sponsors

  • Clerk — the easiest way to add authentication to your application
  • Evomi — residential proxies from $0.49
  • Rapidproxy — residential proxies from $0.55

MIT © Guilherme Rodz