Input OTP

A one-time password input available in two styles — shadcn's joined pill (stock) and individually boxed slots (this registry's custom take).

Open in

Preview

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

export default function BasicExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  const [value, setValue] = React.useState("")

  return (
    <InputOTP
      maxLength={6}
      value={value}
      onChange={setValue}
      variant={variant}
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Installation

pnpm dlx shadcn@latest add https://ui-registry-delta.vercel.app/r/input-otp.json

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"
<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>

Pass variant="joined" to opt into shadcn's stock pill styling. Omit it (or pass "boxed") for this registry's default look.

<InputOTP maxLength={6} variant="joined">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

Composition

Use the following composition to build an InputOTP:

InputOTP
├── InputOTPGroup
│   ├── InputOTPSlot
│   ├── InputOTPSlot
│   └── InputOTPSlot
├── InputOTPSeparator
├── InputOTPGroup
│   ├── InputOTPSlot
│   ├── InputOTPSlot
│   └── InputOTPSlot
├── InputOTPSeparator
└── InputOTPGroup
    ├── InputOTPSlot
    └── InputOTPSlot

Examples

With separator

Split slots into groups with a visual separator.

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

export default function SeparatorExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  return (
    <InputOTP maxLength={6} variant={variant}>
      <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

Restrict input to numeric characters using the pattern prop.

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

export default function DigitsOnlyExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  return (
    <InputOTP maxLength={4} pattern={REGEXP_ONLY_DIGITS} variant={variant}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Disabled

Non-interactive state for read-only or loading contexts.

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

export default function DisabledExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  return (
    <InputOTP maxLength={6} disabled variant={variant}>
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

Controlled

Bind value and onChange to track the OTP in your own state. The live value is displayed below the input.

Enter your one-time password

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

export default function ControlledExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  const [value, setValue] = React.useState("")

  return (
    <div className="flex flex-col items-center gap-4">
      <InputOTP
        maxLength={6}
        value={value}
        onChange={setValue}
        variant={variant}
      >
        <InputOTPGroup>
          <InputOTPSlot index={0} />
          <InputOTPSlot index={1} />
          <InputOTPSlot index={2} />
          <InputOTPSlot index={3} />
          <InputOTPSlot index={4} />
          <InputOTPSlot index={5} />
        </InputOTPGroup>
      </InputOTP>
      <p className="text-center text-sm">
        {value === "" ? (
          "Enter your one-time password"
        ) : (
          <>
            You entered:{" "}
            <span className="font-mono font-medium text-foreground">{value}</span>
          </>
        )}
      </p>
    </div>
  )
}

Invalid

Pass aria-invalid to each InputOTPSlot to show the error styles. Fill all six slots to see the destructive state.

0
0
0
0
0
0
import * as React from "react"
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/components/ui/input-otp"

export default function InvalidExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  const [value, setValue] = React.useState("000000")

  return (
    <InputOTP
      maxLength={6}
      value={value}
      onChange={setValue}
      variant={variant}
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} aria-invalid />
        <InputOTPSlot index={1} aria-invalid />
        <InputOTPSlot index={2} aria-invalid />
        <InputOTPSlot index={3} aria-invalid />
        <InputOTPSlot index={4} aria-invalid />
        <InputOTPSlot index={5} aria-invalid />
      </InputOTPGroup>
    </InputOTP>
  )
}

Alphanumeric

Accept both letters and digits by passing the REGEXP_ONLY_DIGITS_AND_CHARS pattern. Special characters are rejected.

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

export default function AlphanumericExample({
  variant = "boxed",
}: {
  variant?: "boxed" | "joined"
}) {
  return (
    <InputOTP
      maxLength={6}
      pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
      variant={variant}
    >
      <InputOTPGroup>
        <InputOTPSlot index={0} />
        <InputOTPSlot index={1} />
        <InputOTPSlot index={2} />
        <InputOTPSlot index={3} />
        <InputOTPSlot index={4} />
        <InputOTPSlot index={5} />
      </InputOTPGroup>
    </InputOTP>
  )
}

API Reference

InputOTP

Root component. Accepts every prop from the underlying OTPInput in input-otp — the most relevant are listed below.

PropTypeDefaultDescription
variant"boxed" | "joined""boxed"Visual style. "boxed" renders each slot as an individually rounded box (this registry's take); "joined" renders shadcn's stock pill where slots share borders.
maxLengthnumberTotal number of characters the input accepts. Required.
valuestringControlled value of the input.
onChange(value: string) => voidCalled whenever the value changes.
onComplete(value: string) => voidCalled once the user has filled every slot.
patternstringRegex string that restricts allowed characters (e.g. REGEXP_ONLY_DIGITS).
disabledbooleanfalseDisables interaction and dims all slots.
containerClassNamestringClass applied to the slot container, not the hidden input.
classNamestringClass applied to the underlying hidden input element.

InputOTPSlot

A single character slot. Renders the character, active ring, and blinking caret.

PropTypeDefaultDescription
indexnumberZero-based index of the slot within the group. Must be unique and increasing.
classNamestringExtra classes merged onto the slot wrapper.

InputOTPGroup & InputOTPSeparator

Layout helpers. InputOTPGroup wraps related slots; InputOTPSeparator renders a dot between groups. Both forward standard div props.

Accessibility

  • Full keyboard navigation — typing, backspace, arrow keys, and paste all work as expected.
  • Slots expose data-active for focus state and honor aria-invalid for error styling.
  • The separator is rendered with role="separator" so it is announced correctly.