OTP Input

Headless, customizable OTP input for React with SMS autofill, paste handling, keyboard navigation, password masking and right-to-left support.

Features

  • Headless: no styles or wrapper element, style it however you like
  • SMS autofill: iOS and Android can offer the code from a text message
  • Paste anywhere: pasted codes spread across inputs, spaces are ignored
  • Keyboard friendly: auto-advance, Backspace steps back, arrow keys move between inputs
  • Fills in order: clicking an empty input jumps to the first unfilled one
  • Mobile keyboards: type="number" opens the numeric keypad
  • International: Arabic-Indic and Persian digits become 0-9, and it works on right-to-left pages (Arabic, Hebrew) because the code is still entered left to right
  • Multi-character inputs, validation and password masking

Usage

npm install @frjoy/otp

Requires React 18 or newer.

Try it in the Storybook playgroundLive controls for every prop, ready-made examples (SMS code, PIN, license key, Arabic) and interaction tests.

Live Demo 1: Basic OTP

Entered OTP: -

Example.tsx
<Root onChange={setOtp} type="number" password>
  <Label>Enter OTP</Label>
  {[...Array(6)].map((_, i) => (
    <Input key={i} length={1} />
  ))}
</Root>

Live Demo 2: Alphanumeric OTP with Joiner

This example shows a referral-style OTP code joined with -.

Joined Code: -

Example.tsx
<Root onChange={setOtp} type="text" joiner="-">
  <Label>Referral Code</Label>
  {[...Array(4)].map((_, i) => (
    <Input key={i} length={2} />
  ))}
</Root>

Props Reference

  • onChange
    (value: string) => void

    Called with the full code, in input order, whenever it changes. Not called on first render.

  • typedefault 'any'
    'any' | 'number' | 'text'

    number: digits only. text: letters and digits. any: no restriction.

  • passworddefault false
    boolean

    Masks every input.

  • joinerdefault ''
    string

    Placed between input values in onChange, e.g. "-".

  • pattern
    string

    HTML pattern applied to every input. Best with type="any".

  • children
    ReactNode

    Inputs, label and any other elements. Root renders no element of its own.

Accessibility

The component adds no text of its own, so labels stay in your language. Give each input an aria-label:

Example.tsx
<Input aria-label="Digit 1 of 6" />

Interactive Playground

Experiment with different props and see the changes in real-time. For more examples, see the Storybook playground.

Quick Presets

Configuration

Live Preview

Output:
No input yet

Length: 0 characters

Expected total: 6 characters

Try these features:

  • • Paste complete codes, even with spaces - they'll auto-fill
  • • Use arrow keys to navigate between inputs
  • • Backspace deletes and moves to previous input
  • • Click an empty input - focus jumps to the first unfilled one
  • • Arabic-Indic digits like ١٢٣٤ become 1234

Generated Code

VerifyCode.tsx
<Root 
  onChange={setOtp}
>
  <Label>Enter Code</Label>
  <div className="flex gap-1">
    {[...Array(6)].map((_, i) => (
      <Input
        key={i}
        length={1}
        className="min-w-12 px-3 py-2 border border-neutral-300 dark:border-neutral-700 rounded focus:ring-2 focus:ring-blue-500 dark:bg-neutral-800 dark:text-white"
      />
    ))}
  </div>
</Root>

Share this package

Want to improve this page? Create a Pull Request