Input OTP

Input OTP

A one-time-passcode input rendered as character slots with a fake caret. Outline and subtle variants across four slot sizes.

Playground

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

Preview

Compose InputOTP (which owns maxLength and the value) with an InputOTPGroup of indexed InputOTPSlots.

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

Installation

npx shadcn@latest add https://espresso-base-ui.vercel.app/r/input-otp.json

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSeparator,
  InputOTPSlot,
} from "@/components/ui/input-otp"
<InputOTP maxLength={4} onComplete={(code) => verify(code)}>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
  </InputOTPGroup>
</InputOTP>

Variants and sizes

variant and size live on the container and cascade to every slot: outline gives each slot a shadow ring, subtle a filled surface, and sizes run xs through lg.

<InputOTP maxLength={4} variant="outline" size="lg">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
  </InputOTPGroup>
</InputOTP>

<InputOTP maxLength={4} variant="subtle" size="sm">
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
  </InputOTPGroup>
</InputOTP>

Separator

Split the code into groups with InputOTPSeparator, which renders a minus icon between two InputOTPGroups.

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

Invalid

Set aria-invalid on the InputOTPGroup – typically inside a Field with data-invalid="true" – to tint every slot with the error surface.

<Field data-invalid="true">
  <FieldLabel>Verification code</FieldLabel>
  <InputOTP maxLength={6}>
    <InputOTPGroup aria-invalid>
      <InputOTPSlot index={0} />
      <InputOTPSlot index={1} />
      <InputOTPSlot index={2} />
      <InputOTPSlot index={3} />
      <InputOTPSlot index={4} />
      <InputOTPSlot index={5} />
    </InputOTPGroup>
  </InputOTP>
  <FieldError>Invalid code. Please try again.</FieldError>
</Field>

API reference

InputOTP forwards every prop of the underlying input-otp OTPInput – including value/onChange for controlled use, onComplete, pattern and disabled – while the variant props style the rendered slots.

InputOTP

PropTypeDefaultDescription
maxLengthnumber–Required. Number of characters in the code.
variant"outline" | "subtle""outline"Slot surface – shadow ring (outline) or filled secondary background (subtle).
size"xs" | "sm" | "md" | "lg""md"Square slot size (24 / 28 / 32 / 40px) with matching text size and radius; outline trims 2px for its shadow ring.
valuestring–Controlled value; pair with onChange.
onChange(value: string) => void–Called on every value change.
onComplete(value: string) => void–Called when all maxLength characters are filled.
patternstring–Regex the value must match, e.g. "^\\d*$" for digits only.
disabledbooleanfalseDisables input and dims the slots.
containerClassNamestring–Class for the outer container (the element that carries the variant styles); className styles the hidden input.

InputOTPSlot

PropTypeDefaultDescription
indexnumber–Required. Which character of the value this slot displays.
ComponentDescription
InputOTPGroupRow of adjacent slots (data-slot="input-otp-group"). Set aria-invalid here to switch all slots to the error surface.
InputOTPSlotOne character cell with a blinking fake caret when active (data-slot="input-otp-slot", data-active).
InputOTPSeparatorMinus-icon divider with role="separator" between groups (data-slot="input-otp-separator").

Accessibility & styling hooks

Focus lands on a single real input element (rendered invisibly by input-otp), so typing, arrow keys, backspace and pasting a full code all behave natively while the slots are purely visual – the active slot shows a ring plus an animated fake caret. Label the control with a FieldLabel or aria-label, and reflect errors with aria-invalid on the group.

Styling flows from the container: variant and size classes target [data-slot=input-otp-slot] descendants, the active slot exposes data-active="true", and the parts carry data-slot attributes (input-otp, input-otp-group, input-otp-slot, input-otp-separator) for CSS overrides.

Search documentation

Search the component docs