input-otp: one invisible React input behind any OTP UI
One time passcode Input. Accessible & unstyled.
At a glance
- What is it?
- input-otp is an unstyled React component that keeps a single real text field and lets you paint the six boxes yourself. It solves the accessibility and autofill problems that appear when you wire six separate inputs together.
- Who is it for?
- Adopt input-otp if you are building a React OTP screen and want browser autofill, paste and screen reader behaviour to keep working while you control every pixel of the UI. Do not adopt it if you need a styled component out of the box, if you are not on React 16.8 or newer, or if you are targeting React Native, which the repository does not cover.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The six-input OTP field that quietly breaks
HTML has no one-time-password control. There is no input type="otp", so most products assemble one from six separate text inputs and a set of keydown handlers that move focus between them. The README lists what that pattern costs: SMS autofill stops working, screen readers see six unnamed controls instead of one, partial paste into the middle of a half-filled code fails, and keybindings the team never implemented (select-all, word-delete, shift-arrow ranges, undo) simply do not exist.
input-otp is aimed at React developers who are building a verification screen and care about what happens outside the happy path. It is unstyled on purpose. The README's own framing is that it is the accessible, unstyled, fully featured one-time-password component for React, and the repository is published under the MIT license, so it can be dropped into a commercial product without a licensing conversation.
One real input, painted invisible, with slots you draw
The mechanism is the whole point. input-otp renders exactly one real text input, makes it visually invisible, and hands the state to a render prop so you can draw whatever you want on top. Because a text field is still there, everything the browser gives a text field keeps working: autocomplete="one-time-code", one label, one tab stop, one value in FormData, real paste, undo.
The contract is small. maxLength is the number of slots, and render receives the slots array. Each slot carries char, placeholderChar, isActive and hasFakeCaret. The real caret is transparent, so you draw your own. That is why the component ships no theme and no CSS to import: there is nothing to override because there is nothing to begin with.
What makes this more than a thin wrapper is the list of edge cases the README documents. A collapsed caret has no slot, so 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 because direction is inferred from the previous selection. Deleting does not fire selectionchange, so the event is dispatched by hand when the value shrinks. Password manager badges that cover the last slot are detected by known extension markers and then by probing the field's top-right corner, and the input widens 40px behind a clip-path so there is no visible layout shift. iOS refuses to paste into an invisible input, so the field keeps opacity: 1 and hides itself with transparent colours instead, with paste handled manually. Autofill paints its own background, so :autofill is neutralised and the state is shaken off with a synthetic input event. With JavaScript disabled, a noscript stylesheet turns the input back into a plain visible one.
That table is the honest description of the project's value. The API is five props; the work is in the twenty-odd behaviours underneath it.
Installing input-otp and rendering a first code field
The package installs from npm. The README gives this as the install step:
npm install input-otpA minimal client component passes maxLength and a render function. The README's usage example looks like this:
'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. The README's Slot component reads char, placeholderChar, isActive and hasFakeCaret, and the comment in the example is explicit that the real caret is transparent, which is why you render a fake one:
import type { SlotProps } from 'input-otp'
function Slot({ char, placeholderChar, isActive, hasFakeCaret }: SlotProps) {
return (
<div className={isActive ? 'z-10 outline-2' : ''}>
{char ?? placeholderChar}
{hasFakeCaret && <FakeCaret />}
</div>
)
}What you should see after wiring this up: six boxes, a moving highlight on the active slot, and a code that fills left to right as you type or paste. The README points to the Installation page of the docs for the full copy-pasteable slot component, including the caret keyframe and the Stripe-style dash, so the two snippets above are a starting point rather than the finished visual.
If your project already uses shadcn/ui, that project ships its own input-otp component wrapping this library with pre-composed parts. The README gives the command:
npx shadcn@latest add input-otpThe difference is the surface: you use InputOTPSlot index={0} instead of writing a render prop yourself. The engine underneath is the same, so the edge case handling described above still applies.
Where input-otp is the wrong choice
The first limitation is the one the project states about itself: it is unstyled. If you want a finished-looking OTP field with no design work, this is not that. You will write the slot markup, the caret, the active ring and the focus styles. The README's own example uses cn() and Tailwind classes, and the full slot component lives on the documentation site rather than in the package, so the visual layer is your responsibility either way.
The second is the platform. The repository is a React component; the topics list 2fa, mfa, otp, otp-verification and react, and the package description in package.json is a one-time password input component for React. Nothing in the repository describes a React Native build, a vanilla JavaScript build or a Vue build. If you are shipping a native app, this library is not the answer, and the search results for this project's name include people asking about React Native precisely because the boundary is not obvious from the name.
The third is the quantity of behaviour you are inheriting. The edge case list is long because the problem is genuinely messy: selectionchange rewriting, synthetic input events, clip-path widening for password manager badges, a noscript stylesheet. That is the reason to use the library, and also the reason to test it inside your own layout. A 40px widening behind a clip-path assumes a container that can absorb it. A synthetic input event to shake off autofill styling assumes your form handler tolerates it. These are reasonable assumptions, but they are assumptions about your page, not guarantees.
Finally, the component is client-side. The README's example opens with 'use client', which matters if you are in a React Server Components framework and expect to import it from a server component without a boundary.
input-otp against hand-rolled inputs and headless primitives
The obvious alternative is what most teams do first: six controlled inputs, a ref array, and keydown handlers for backspace and arrows. That approach is not wrong, and for a throwaway internal tool it is faster than reading anyone's documentation. The difference in approach is structural, not cosmetic. With six inputs, autofill, paste and screen reader output are things you have to reimplement, and the browser treats each box as its own field with its own name and its own tab stop. With input-otp, there is one field, so those behaviours come from the browser rather than from your handlers. The cost is that you must render the visuals yourself from slot state, which is exactly the work the six-input version avoids.
A second comparison point is the shadcn/ui input-otp component mentioned above. It is not really a competitor: the README describes it as wrapping this library with pre-composed parts. Choosing it means choosing this engine plus a set of styled components, which is a reasonable middle path if you want the accessibility behaviour without writing slot markup. The trade-off is that you inherit shadcn/ui's component conventions and update cadence alongside input-otp's.
The third option is a general-purpose headless input primitive. Those give you focus management and composition handling, but they do not know what an OTP is. Nothing in them rewrites the selection into a one-character range on selectionchange, and nothing in them widens the field to dodge a password manager badge. That specificity is the reason this package exists as a separate library rather than as a pattern in a blog post.
Maintenance, releases and what the MIT licence leaves you
The repository is not archived, and the last push was on 2026-09-10. The most recent release is v1.5.0, published on 2026-08-18, preceded by v1.5.0-beta.2 the same day and v1.5.0-beta.1 on 2026-07-27. The versioning pattern visible in package.json's scripts is a beta line promoted to a stable release, with release:beta:first, release:beta, release:patch, release:minor and release:major all routed through scripts/release.mjs, plus a release:dry variant for a dry run. That is a maintainer with a defined release process, not ad-hoc publishing.
Upgrade cost is shaped by the API surface. The README describes the API as five props and the render contract as slots in, markup out. A contract that small is cheap to keep working across versions, and the changelog is the place to confirm whether any given upgrade touches the slot shape. The repository is a pnpm workspace with turbo, packages/ and apps/, so if you are contributing rather than consuming, the build and test entry points are the root scripts: build:lib filters to input-otp, test filters to playground, and type-check runs across the workspace.
The licence is MIT, declared in the repository and shown by the npm badge in the README. In practical terms that permits commercial use and modification with the licence and copyright notice retained. This is a description of what the licence identifier means, not legal advice; if your organisation has a policy on third-party licences, run it through that policy.
Editorial conclusion
Adopt input-otp if you are building a React OTP screen and want browser autofill, paste and screen reader behaviour to keep working while you control every pixel of the UI. Do not adopt it if you need a styled component out of the box, if you are not on React 16.8 or newer, or if you are targeting React Native, which the repository does not cover. Before you commit, verify two things in your own build: that your bundler resolves the package's client directive if you are on a React Server Components framework, and that your password manager extension does not cover the last slot in the browsers your users actually run.
Frequently asked questions
What is input-otp?
It is an accessible, unstyled one-time-password component for React, published under the MIT licence. It renders exactly one real text input, paints it invisible, and gives you slot state so you can draw your own UI on top.
How do I install input-otp in a React project?
The README gives npm install input-otp as the install step. You then render OTPInput with a maxLength for the number of slots and a render function that maps over the slots array.
Does input-otp work with shadcn/ui?
Yes. The README states that shadcn/ui's input-otp component wraps this library with pre-composed parts, so you use InputOTPSlot index={0} instead of a render prop. The install command given is npx shadcn@latest add input-otp.
Can I use input-otp with React Native?
The repository does not describe a React Native build. The package is a React component for the web, and the README's examples use DOM-oriented props such as autocomplete="one-time-code" and a noscript stylesheet.
Does input-otp keep SMS autofill working?
The README states that autocomplete="one-time-code" only means something on a single field, which is why the component renders one input rather than six. That is the mechanism behind the autofill claim.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/guilhermerodz-input-otp)