Free shadcn/ui input OTP for React — a one-time-password field with accessible, copy-paste-friendly slots: a six-slot default, grouped slots with a separator, digit-only input, a controlled value, and a verify card.

Installation

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

input-otp
utils

Props

maxLength* = —
number

Number of slots (e.g. 6 for a 6-digit code).

pattern = —
string

A regex of allowed chars (e.g. digits only).

value / onChange = —
string / (v: string) => void

Controlled value of the entered code.

A one-time-password / verification-code input built on the input-otp library (by @guilhermerodz). Set maxLength to the number of digits and compose <InputOTP> with <InputOTPGroup> of <InputOTPSlot index={n} />, optionally split by an <InputOTPSeparator>. Behaves like one input: paste fills it, arrows move between slots. Pass pattern (REGEXP_ONLY_DIGITS) to restrict input. The active slot shows a blinking caret (animate-caret-blink).

Copied!
components/ui/input-otp.tsx
"use client"

import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"

import { cn } from "@/lib/utils"

function InputOTP({
  className,
  containerClassName,
  ...props
}: React.ComponentProps<typeof OTPInput> & {
  containerClassName?: string
}) {
  return (
    <OTPInput
      data-slot="input-otp"
      containerClassName={cn(
        "flex items-center gap-2 has-disabled:opacity-50",
        containerClassName
      )}
      className={cn("disabled:cursor-not-allowed", className)}
      {...props}
    />
  )
}

function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-otp-group"
      className={cn("flex items-center", className)}
      {...props}
    />
  )
}

function InputOTPSlot({
  index,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  index: number
}) {
  const inputOTPContext = React.useContext(OTPInputContext)
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}

  return (
    <div
      data-slot="input-otp-slot"
      data-active={isActive}
      className={cn(
        "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
        className
      )}
      {...props}
    >
      {char}
      {hasFakeCaret && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
          <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
        </div>
      )}
    </div>
  )
}

function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
  return (
    <div data-slot="input-otp-separator" role="separator" {...props}>
      <MinusIcon />
    </div>
  )
}

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

Examples

Basic

The canonical six-slot one-time-password input.

Packages

input-otp
input-otp

Props

No props documented yet.

Copied!
components/ui/input-otp-demo-01.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOTPDemo01() {
  return (
    <InputOTP maxLength={6}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

With separator

Two groups of three slots split by a separator.

Packages

input-otp
input-otp

Props

No props documented yet.

Copied!
components/ui/input-otp-separator-01.tsx
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOTPSeparator01() {
  return (
    <InputOTP maxLength={6}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
      </InputOTPGroup>
      <InputOTPSeparator />
      <InputOTPGroup>
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Digits only

Restricts input to numbers with the REGEXP_ONLY_DIGITS pattern.

Packages

input-otp
input-otp

Props

No props documented yet.

Copied!
components/ui/input-otp-pattern-01.tsx
"use client"

import { REGEXP_ONLY_DIGITS } from "input-otp"

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOTPPattern01() {
  return (
    <div className="space-y-2 text-center">
      <InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
        <InputOTPGroup>
          {Array.from({ length: 6 }).map((_, i) => (
            <InputOTPSlot key={i} index={i} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <p className="text-muted-foreground text-xs">Digits only</p>
    </div>
  )
}

Controlled

A controlled OTP that echoes the entered value as you type.

Packages

input-otp
input-otp

Props

No props documented yet.

Copied!
components/ui/input-otp-controlled-01.tsx
"use client"

import * as React from "react"

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOTPControlled01() {
  const [value, setValue] = React.useState("")

  return (
    <div className="space-y-3 text-center">
      <InputOTP maxLength={6} value={value} onChange={setValue}>
        <InputOTPGroup>
          {Array.from({ length: 6 }).map((_, i) => (
            <InputOTPSlot key={i} index={i} />
          ))}
        </InputOTPGroup>
      </InputOTP>
      <div className="text-muted-foreground text-sm">
        {value === "" ? "Enter your code" : `You entered: ${value}`}
      </div>
    </div>
  )
}

Verify card

An OTP in a verification card with a submit that stays disabled until all slots are filled.

Packages

input-otp
input-otp
button
card

Props

No props documented yet.

Copied!
components/ui/input-otp-form-01.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InputOTPForm01() {
  const [value, setValue] = React.useState("")

  return (
    <Card className="w-full max-w-sm">
      <CardHeader className="items-center text-center">
        <CardTitle>Verify your email</CardTitle>
        <CardDescription>
          Enter the 6-digit code we sent to your inbox.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex justify-center">
        <InputOTP maxLength={6} value={value} onChange={setValue}>
          <InputOTPGroup>
            <InputOTPSlot index={0} />
            <InputOTPSlot index={1} />
            <InputOTPSlot index={2} />
          </InputOTPGroup>
          <InputOTPSeparator />
          <InputOTPGroup>
            <InputOTPSlot index={3} />
            <InputOTPSlot index={4} />
            <InputOTPSlot index={5} />
          </InputOTPGroup>
        </InputOTP>
      </CardContent>
      <CardFooter>
        <Button className="w-full" disabled={value.length < 6}>
          Verify
        </Button>
      </CardFooter>
    </Card>
  )
}

Frequently asked questions

It is an accessible one-time-password (OTP) input built on the input-otp library. It renders a row of single-character slots that behave like one field — typing advances, backspace retreats, and pasting a full code fills every slot at once. It is the component you reach for on a verify-email, two-factor, or SMS-code screen.
Yes. The slots are backed by a single hidden input, so pasting a full code fills every slot in one go — including codes pasted from an autofilled SMS. That single-input model is also what makes screen-reader and mobile keyboard behaviour correct.
Import the ready-made pattern and pass it: import { REGEXP_ONLY_DIGITS } from "input-otp", then . The library also ships REGEXP_ONLY_CHARS and REGEXP_ONLY_DIGITS_AND_CHARS, or you can pass your own regex string.
Set maxLength on InputOTP and render that many InputOTPSlot elements with matching index props. A four-slot PIN is maxLength={4} with slots 0–3; a six-slot code is maxLength={6} with slots 0–5. Group them with InputOTPGroup and split groups with InputOTPSeparator.
Control it: const [value, setValue] = React.useState(""), then . value is the full string; onComplete fires once when every slot is filled, which is the natural place to auto-submit or enable the verify button.
Each slot reads its state from the OTPInputContext and renders a fake caret (animate-caret-blink) on the active slot, since a real text caret cannot span separate boxes. The blink keyframe ships with tw-animate-css, so the caret works without extra CSS.