Introduction

One invisible input, any UI you can imagine. The design decisions behind the most complete OTP field on the web.

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 in FormData, required and disabled that 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 selectionchange in 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 :autofill is neutralised and the state is shaken off with a synthetic input event.
  • 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

Where to go next#