# Shadcn Input OTP

> 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.

Source: https://designrevision.com/components/input-otp

The **Shadcn Input OTP** is a one-time-password field for React — a row of single-character **slots** that behave like one input. Typing advances, backspace retreats, and pasting a full code fills every slot at once. It is built on the <a href="https://github.com/guilhermerodz/input-otp" rel="nofollow">input-otp</a> library, which backs the slots with a single hidden input so paste, autofill, and screen readers all work correctly. The examples below cover a basic six-slot code, grouped slots, digit-only input, a controlled value, and a verify card.

## The parts

```tsx
<InputOTP maxLength={6}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    {/* … */}
  </InputOTPGroup>
</InputOTP>
```

`InputOTP` owns the value and `maxLength`; `InputOTPGroup` wraps a run of slots; each `InputOTPSlot` renders one character box keyed by `index`. `InputOTPSeparator` drops a divider between groups.

## Paste just works

Because every slot reads from one hidden input, pasting a full code — including an autofilled SMS code — fills all the boxes at once. You don't wire anything up for it.

## Digits only

Pass one of the built-in patterns:

```tsx
import { REGEXP_ONLY_DIGITS } from "input-otp"

<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>
```

The library also ships `REGEXP_ONLY_CHARS` and `REGEXP_ONLY_DIGITS_AND_CHARS`, or pass your own regex string.

## Reading the code

Control it and act when it completes:

```tsx
const [value, setValue] = React.useState("")

<InputOTP
  maxLength={6}
  value={value}
  onChange={setValue}
  onComplete={(code) => verify(code)}
>
```

`onComplete` fires once every slot is filled — the natural place to auto-submit or enable a verify button (the **Verify card** example does exactly that).

## Grouping

Split a long code into readable chunks with a separator:

```tsx
<InputOTPGroup>{/* slots 0–2 */}</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>{/* slots 3–5 */}</InputOTPGroup>
```

## Accessibility

The single hidden input means the field is announced as one control, the mobile keyboard shows the right type, and the active slot gets a fake blinking caret (`animate-caret-blink`) since a real caret can't span separate boxes. Pair it with a `<Label>` or surrounding copy that says what the code is for.

## Installation

### input-otp

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp
```

**Dependencies:** input-otp, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| 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. |

`*` required.

**Usage & accessibility:** 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).

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp-demo-01
```

**Dependencies:** input-otp, input-otp

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp-separator-01
```

**Dependencies:** input-otp, input-otp

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp-pattern-01
```

**Dependencies:** input-otp, input-otp

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp-controlled-01
```

**Dependencies:** input-otp, input-otp

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/input-otp-form-01
```

**Dependencies:** input-otp, input-otp, button, card

```tsx
// 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>
  )
}
```
