Skip to content
Primitives

Input OTP

Segmented one-time-code field with sharp mono cells, a 2px signal focus ring on the active slot, and a blinking block caret.

● LIVE
npx shadcn@latest add https://okibi.cndr.dev/r/input-otp.json

Installation

Install the theme first, then add the component:

npx shadcn@latest add https://okibi.cndr.dev/r/input-otp.json

Install the required dependencies:

npm install input-otp lucide-react

Copy the source from the Code tab above into components/ui/input-otp.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.

Usage

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

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

Examples

With separator

Split the slots into groups with an InputOTPSeparator for a grouped code layout (e.g. 3-3).

● LIVE

Invalid

Set aria-invalid and every slot flips to the destructive keyline; pair it with a mono rejection line.

● LIVE
3
1
9

AUTH // code rejected — expired or incomplete.

Disabled

The native disabled attribute dims and locks the whole field.

● LIVE
7
7
4
1

API Reference

InputOTP

PropTypeDefaultDescription
maxLengthnumberTotal number of slots / code length.
valuestringControlled value.
onChange(value: string) => voidFires as the code is entered.
patternstringRegex the input is validated against (e.g. digits only).

InputOTPSlot

PropTypeDefaultDescription
indexnumberWhich slot this cell renders (0-based).

Accessibility

  • Typing is captured by one hidden input behind the slots; the visible cells are presentational, so paste and arrow keys behave like a single field.
  • Setting aria-invalid on InputOTP is propagated through context down to every InputOTPSlot — the destructive border renders on the cells, not just the hidden input.
  • The active slot shows a 2px inset ring and a blinking block caret; the caret animation is reduced-motion gated globally.
  • InputOTPSeparator is role='separator'; keep its icon decorative so it isn't announced as content.
  • Disabling the field dims the whole group via has-disabled and blocks pointer interaction.

Guidelines

Do

  • Set maxLength to the exact code length and pass a digits-only pattern when the code is numeric.
  • Drive validation by setting aria-invalid on InputOTP so the error border reaches every slot.
  • Pair the field with a visible label and announce success once the code is complete.

Don't

  • Don't push past ~6–8 slots; the fixed-size cells overflow narrow viewports with no wrap.
  • Don't style individual slots for error by hand — let the aria-invalid context drive it so every cell stays consistent.

On this page