Installation

Add input-otp to a React app and render your first field in about a minute.

input-otp is a single component with no dependencies beyond React. There is no provider to mount, no CSS file to import and no config step.

Install the package

$ pnpm add input-otp

React 16.8 or newer is the only peer dependency, up to and including React 19.

Render a field

maxLength is the number of slots. render receives them and returns your markup — that's the entire contract.

'use client'
 
import { OTPInput } from 'input-otp'
 
export function VerificationCode() {
  return (
    <OTPInput
      maxLength={6}
      render={({ slots }) => (
        <div style={{ display: 'flex', gap: 4 }}>
          {slots.map((slot, idx) => (
            <div key={idx} style={{ width: 40, height: 52, border: '1px solid #333' }}>
              {slot.char}
            </div>
          ))}
        </div>
      )}
    />
  )
}

Make it look like something

The starter above is deliberately ugly. Below is the slot component used throughout these docs — copy it into your project as components/ui/otp-slot.tsx and you have a field you can ship.

components/ui/otp-slot.tsx
import * as React from 'react'
import type { SlotProps } from 'input-otp'
 
import { cn } from '@/lib/utils'
 
/**
 * The slot: one visible cell of the field.
 *
 * `input-otp` never renders this — it hands you `char`, `placeholderChar`,
 * `isActive` and `hasFakeCaret` and gets out of the way. Everything below is
 * plain markup you own.
 */
export function Slot({
  char,
  placeholderChar,
  isActive,
  hasFakeCaret,
  className,
}: SlotProps & { className?: string }) {
  return (
    <div
      className={cn(
        'relative flex h-14 w-12 items-center justify-center',
        'text-[1.375rem] font-medium tabular-nums text-foreground',
        'border-y border-r border-foreground/[0.18] bg-foreground/[0.02]',
        'first:rounded-l-md first:border-l last:rounded-r-md',
        'transition-all duration-200',
        // The group-* hooks come from `containerClassName="group …"`.
        'group-hover:border-foreground/30 group-focus-within:border-foreground/30',
        'outline outline-0 outline-offset-0 outline-foreground/20',
        isActive && 'z-10 outline-2 outline-foreground/80',
        className,
      )}
    >
      {/* Placeholder characters are dimmed via a data attribute the library
          sets on the real input while the value is empty — no JS needed. */}
      <div className="group-has-[input[data-input-otp-placeholder-shown]]:text-muted-foreground/40">
        {char ?? placeholderChar}
      </div>
 
      {hasFakeCaret && <FakeCaret />}
    </div>
  )
}
 
/** The blinking bar. The real caret is transparent, so we draw our own. */
export function FakeCaret() {
  return (
    <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
      <div className="h-7 w-px bg-foreground motion-safe:animate-caret-blink" />
    </div>
  )
}
 
/** Stripe-style dash between two groups of slots. */
export function FakeDash() {
  return (
    <div aria-hidden className="flex w-10 items-center justify-center">
      <div className="h-1 w-3 rounded-full bg-border" />
    </div>
  )
}
 
export function SlotGroup({
  children,
  className,
}: {
  children: React.ReactNode
  className?: string
}) {
  return <div className={cn('flex', className)}>{children}</div>
}

It expects Tailwind, the cn helper from shadcn/ui, and one keyframe for the blinking caret:

// tailwind.config.ts
export default {
  theme: {
    extend: {
      keyframes: {
        'caret-blink': {
          '0%,70%,100%': { opacity: '1' },
          '20%,50%': { opacity: '0' },
        },
      },
      animation: {
        'caret-blink': 'caret-blink 1.2s ease-out infinite',
      },
    },
  },
}

Wire it up

Which leaves you here:

Already using shadcn/ui?#

shadcn/ui's input-otp component is a thin wrapper around this library — same engine, pre-composed parts. If you want <InputOTPSlot index={0} /> instead of a render prop, install it from the registry:

npx shadcn@latest add input-otp

It uses the Context API form of this component under the hood, so everything in these docs still applies.

Editor setup#

containerClassName won't get Tailwind IntelliSense out of the box because it isn't named className. One setting fixes it for every *ClassName prop:

// .vscode/settings.json
{
  "tailwindCSS.classAttributes": ["class", "className", ".*ClassName"]
}

Verifying the install#

A field that's wired up correctly should pass all of these on the first try — if any of them fail, something in the setup is off:

  • Clicking anywhere in the row of slots focuses the field and puts the caret in a sensible place.
  • ⌘A selects the whole code, and typing replaces it.
  • Pasting a full code fills every slot at once.
  • Tabbing away removes the active-slot highlight; tabbing back restores it at the end of the value.

Next#

Anatomy shows what you just rendered, from the inside.