HTML has no one-time-password control. There is no <input type="otp">, so every product that asks for a six-digit code has to invent one — and almost everybody invents the same thing: six separate inputs, wired together with keydown handlers that shuffle focus between them.
That approach breaks in ways that are easy to miss and hard to fix. input-otp takes the opposite route. It renders exactly one real text input, paints it invisible, and hands you the state you need to draw whatever you want on top.
Why one input#
Everything a browser gives a text field for free — and there is a surprising amount — keeps working, because there is still a text field:
- SMS autofill.
autocomplete="one-time-code"only means anything on a single field. iOS and Android will drop the whole code straight in. - Screen readers. One input has one accessible name, one value and one caret. Six inputs announce six unlabelled boxes and lose the reader on every focus jump.
- Every keybinding you never implemented. Select-all, word-delete, shift-arrow ranges, undo, the iOS long-press menu, the Android clipboard bar, middle-click paste on Linux. All native.
- Copy, cut and paste. Including partial pastes into the middle of a half-filled code, which the focus-shuffling approach essentially cannot do.
- Form semantics. One
name, one value inFormData,requiredanddisabledthat mean what they say, and a real<label>that focuses it.
What you give up is the ability to style that input directly — so the library gives it back as render state. Anatomy takes the lid off and shows you the whole mechanism.
What you write#
There is one component. You tell it how long the code is, and you get an array of slots back — each one carrying its character, whether it is selected, and whether it should be drawing a caret. The markup is yours.
Slot above is a plain div you write once and own forever — Installation has the full source to copy.
What it handles for you#
The interesting part of this library is not the API — it is the list of things that go wrong when you try to make one input look like six, and the fixes for each. A sample:
- A collapsed caret is ambiguous between two slots, so the selection is rewritten into a one-character range on every
selectionchange— while still allowing a true insert caret at the end. - Deleting text doesn't fire
selectionchangein any browser, so the event is dispatched by hand. - Password manager badges land on top of your last slot, so known extensions are detected and the input widens by 40px behind a clip-path to walk the badge out — with no visible layout shift.
- iOS paints the selection and caret in a native layer no CSS can hide, and refuses to show the long-press paste menu on a zero-opacity input — so a dedicated set of iOS-only rules parks the text offscreen, scales the field down 10x, and a paste handler does the insertion by hand.
- Autofill paints its own background colour over a field you asked to be transparent, so
:autofillis neutralised and the state is shaken off with a syntheticinputevent. - With JavaScript disabled, an invisible input is an unusable input — so a
<noscript>stylesheet turns it back into an ordinary visible one.
Each of these is written up with the reasoning and the exact fix in Edge cases.
At a glance#
Bundle size
~4 kB
minified + gzipped, zero dependencies
React
16.8 → 19
hooks-era and up, RSC-friendly
Styling
None
bring your own, or copy a recipe