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/otpRequires 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: -
<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: -
<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) => voidCalled 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
booleanMasks every input.
- joinerdefault ''
stringPlaced between input values in onChange, e.g. "-".
- pattern
stringHTML pattern applied to every input. Best with type="any".
- children
ReactNodeInputs, label and any other elements. Root renders no element of its own.
| Prop / Type | Default | Description |
|---|---|---|
onChange (value: string) => void | — | Called with the full code, in input order, whenever it changes. Not called on first render. |
type 'any' | 'number' | 'text' | 'any' | number: digits only. text: letters and digits. any: no restriction. |
password boolean | false | Masks every input. |
joiner 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:
<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
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
<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>