mrkeyoor.com_
Thu 06 Aug 02:44 UTC
npmSecurityupdated 06 Aug 2026

input-otp

input-otp is a React component for one-time-code fields, the six boxes you type an SMS or authenticator code into. HTML has no control for this, so the usual build is six separate inputs wired together with keydown handlers that move focus around, and that approach quietly breaks SMS autofill, screen readers, partial paste, undo, and half the keyboard shortcuts. input-otp renders one real text input, makes it visually transparent, and gives you the per-slot state so you can draw whatever boxes you want on top. Because a single text field is still there, autocomplete="one-time-code", form submission, labels, and the platform keyboard behaviour all keep working. You give it maxLength for the number of slots and a render function that receives the slots; it gives back char, placeholderChar, isActive, and hasFakeCaret for each one. It ships no styles and has zero dependencies.

Verdict

The best-argued OTP input for React: one real field, so autofill, paste, and screen readers work, at under 4 KB with no dependencies. Accept that you are writing the visuals yourself and that the stable release trails the documentation.

API stability5/5Five props and a render function have carried the whole API through the 1.x line, and every other input attribute is simply forwarded, so upgrades within 1.x have not required code changes.
Docs5/5A dedicated docs site covers installation, styling, validation, forms, accessibility, password managers with a live simulator, mobile quirks, and a full API reference, and the README explains the reasoning behind each edge case. The catch is that it documents the 1.5 line rather than the stable release.
Maintenance4/5Pushed July 2026 with only three open issues, and a 1.5.0-beta.1 in progress, but the last stable release was January 2025, so anything fixed since then is only available by opting into the beta.
Ecosystem5/5Around 33.1M weekly downloads, driven largely by shadcn/ui shipping it as the engine behind its own input-otp component, which means most React OTP fields you meet in other codebases are already this library.

Use it if

  • You want iOS and Android SMS autofill to work: the one-time-code autocomplete hint only fires on a single field, so any six-input implementation loses it
  • Accessibility is a requirement. One input means one tab stop, one accessible name, one value, and one caret, instead of six controls a screen reader reads as six unrelated text boxes
  • You want the field to look exactly like your design: separators, group gaps, animated carets, per-slot states are all yours to draw, and the package imposes no CSS at all
  • You need real paste behaviour, including pasting a code into a half-filled field, plus undo, select-all, and shift-arrow selection working the way users expect
  • You already use shadcn/ui, whose input-otp component is a thin wrapper over this library, so you are running it either way
Skip it if

Setup reality

npm install input-otp, add 'use client' if you are in the Next.js App Router, and render an OTPInput with maxLength and a render prop. There are no dependencies and no CSS import. The real work starts after that: you write the slot component, the border and rounding rules for first and last slot, the active-slot ring, and the keyframes for the fake caret, because the transparent input has no visible caret of its own. Two smaller traps. The published TypeScript types inherit pattern from the standard input attributes and declare it as a string, even though the runtime happily accepts a RegExp, so pass one of the exported REGEXP_ONLY_DIGITS style constants and avoid the argument. And password manager badge handling defaults to widening the input by 40 pixels behind a clip-path, which is usually invisible but can be surprising if you measure the container yourself.

Patterns

A six-slot code fieldbasic-otp-field

'use client';
import { OTPInput } from 'input-otp';

export function CodeField() {
  return (
    <OTPInput
      maxLength={6}
      containerClassName="flex items-center gap-2"
      render={({ slots }) => (
        <div className="flex">
          {slots.map((slot, i) => <Slot key={i} {...slot} />)}
        </div>
      )}
    />
  );
}

maxLength is the slot count. containerClassName styles the visible wrapper; a plain className goes to the invisible input, which is almost never what you want.

Draw one slot, including the fake caretslot-component

import type { SlotProps } from 'input-otp';

function Slot({ char, placeholderChar, isActive, hasFakeCaret }: SlotProps) {
  return (
    <div
      className={[
        'relative flex h-14 w-12 items-center justify-center border text-xl',
        isActive ? 'ring-2 ring-offset-0' : '',
      ].join(' ')}
    >
      {char ?? placeholderChar}
      {hasFakeCaret && <span className="animate-caret-blink h-8 w-px bg-current" />}
    </div>
  );
}

The real caret is transparent, so hasFakeCaret is the only signal that this slot is where typing lands. Without an animation on that element the field looks frozen while focused.

Control the value from statecontrolled-value

const [code, setCode] = useState('');

<OTPInput
  maxLength={6}
  value={code}
  onChange={setCode}
  render={({ slots }) => <Slots slots={slots} />}
/>

onChange receives the string itself, not a change event, so setCode passes straight in. Feeding back a value longer than maxLength is ignored rather than truncated silently at the source.

Submit as soon as the code is fullauto-submit-on-complete

const [pending, setPending] = useState(false);

<OTPInput
  maxLength={6}
  disabled={pending}
  onComplete={async (value) => {
    setPending(true);
    try {
      await verifyCode(value);
    } finally {
      setPending(false);
    }
  }}
  render={({ slots }) => <Slots slots={slots} />}
/>

onComplete fires once on the transition to a full value, not on every keystroke afterwards. Disable the field while the request is in flight or a fast second paste fires a duplicate verification.

Restrict input to digitsdigits-only

import { OTPInput, REGEXP_ONLY_DIGITS } from 'input-otp';

<OTPInput
  maxLength={6}
  pattern={REGEXP_ONLY_DIGITS}
  inputMode="numeric"
  render={({ slots }) => <Slots slots={slots} />}
/>

pattern gates every change, including paste, and there is no default, so a field with no pattern accepts any character. REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS are exported too. inputMode only picks the mobile keyboard; it enforces nothing.

Accept codes pasted with spaces or dashesclean-pasted-codes

<OTPInput
  maxLength={6}
  pattern={REGEXP_ONLY_DIGITS}
  pasteTransformer={(pasted) => pasted.replace(/[\s-]/g, '')}
  render={({ slots }) => <Slots slots={slots} />}
/>

Email clients and SMS apps hand over codes like "123 456". Without the transformer the pattern rejects the paste outright and the user sees nothing happen at all.

Use it in a plain form or a server actionform-integration

<form action={verifyAction}>
  <label htmlFor="otp">Verification code</label>
  <OTPInput
    id="otp"
    name="otp"
    required
    maxLength={6}
    render={({ slots }) => <Slots slots={slots} />}
  />
  <button type="submit">Verify</button>
</form>

Because there is one real input, name produces one FormData entry and a real label focuses it. Every unrecognised prop is forwarded to the input, so required, autoFocus, and aria attributes work as normal.

Wire it into react-hook-formreact-hook-form

import { Controller, useForm } from 'react-hook-form';

const { control, handleSubmit } = useForm({ defaultValues: { otp: '' } });

<Controller
  control={control}
  name="otp"
  rules={{ minLength: 6, required: true }}
  render={({ field }) => (
    <OTPInput
      maxLength={6}
      value={field.value}
      onChange={field.onChange}
      onBlur={field.onBlur}
      ref={field.ref}
      render={({ slots }) => <Slots slots={slots} />}
    />
  )}
/>

Controller is needed because onChange hands over a string rather than an event. The ref points at the real input, so react-hook-form can focus the field when validation fails.

Compose slots instead of using a render propcomposition-with-context

import { OTPInput, OTPInputContext } from 'input-otp';
import { useContext } from 'react';

function Slot({ index }: { index: number }) {
  const { slots } = useContext(OTPInputContext);
  const slot = slots[index];
  return <div data-active={slot.isActive}>{slot.char}</div>;
}

<OTPInput maxLength={6}>
  <Slot index={0} />
  <Slot index={1} />
  <span>-</span>
  <Slot index={2} />
</OTPInput>

Pass children instead of render and read OTPInputContext inside them. This is how shadcn/ui builds its InputOTPSlot and InputOTPSeparator parts; render and children are mutually exclusive.

Show placeholder characters in empty slotsplaceholder-slots

<OTPInput
  maxLength={6}
  placeholder="000000"
  textAlign="center"
  render={({ slots }) => (
    <div className="flex">
      {slots.map((slot, i) => (
        <Slot key={i} {...slot} />
      ))}
    </div>
  )}
/>

Each slot receives its own placeholderChar, so render char ?? placeholderChar and style the placeholder state with a muted colour. textAlign changes where the caret sits when the field is empty.

Clear the field and refocus after a wrong codefocus-and-reset

const inputRef = useRef<HTMLInputElement>(null);
const [code, setCode] = useState('');

async function onComplete(value: string) {
  const ok = await verifyCode(value);
  if (!ok) {
    setCode('');
    inputRef.current?.focus();
  }
}

<OTPInput ref={inputRef} value={code} onChange={setCode} maxLength={6}
  onComplete={onComplete} render={({ slots }) => <Slots slots={slots} />} />

ref is the actual input element, so the normal DOM methods apply. Clearing state alone leaves focus where it was, which on mobile can dismiss the keyboard and strand the user.

Strict CSP and password manager badgescsp-and-password-managers

<OTPInput
  maxLength={6}
  // no nonce prop in 1.4.2: drop the injected <noscript> styles instead
  noScriptCSSFallback={null}
  // stop the field widening to dodge 1Password and LastPass badges
  pushPasswordManagerStrategy="none"
  render={({ slots }) => <Slots slots={slots} />}
/>

The default noscript style block makes the input visible when JavaScript is off, but it is inline CSS that a strict style-src rejects. Setting the strategy to none keeps your layout measurements exact at the cost of a password manager badge possibly covering the last slot.

Alternatives

PackageRegistryPick it when
react-otp-inputnpmYou want the traditional multiple-input component with styling props and no render-prop work.
@radix-ui/react-one-time-password-fieldnpmYou already build on Radix primitives and want their composition and accessibility conventions.
react-hook-formnpmThe OTP field is one part of a larger form and validation and submission wiring is the actual problem.