v0.5

Input OTP

One-time-code input that splits each character into its own slot.

Description

InputOTP is a compound component built on the input-otp package's OTPInput. Under the hood it renders a single focusable input, but visually it splits each character into its own slot so the user can see and edit one digit at a time. Sub-parts (InputOTPGroup, InputOTPSlot, InputOTPSeparator) compose the layout.

Use it for fixed-length codes: 2FA verification, email or SMS confirmation, phone verification, account recovery. The slot layout signals to the user how many characters are expected and where the separator (if any) falls. When wrapped in Field, the OTP picks up size, invalid, and disabled automatically.

Don't use it for free-form text. For variable-length input use Input. For long formatted IDs like license keys or IBAN use Input with a mask helper. For a numeric value with min and max, use InputStepper.

Installation

pnpm dlx @create-ui/cli add input-otp

Anatomy

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

Usage

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

Examples

Variants

variant sets the surface fill. outline (default) has a transparent fill and a visible border. filled adds a subtle background that lifts the slot off the surrounding surface.

Outline
Filled

Shape

shape controls the slot radius and border style. rounded (default) follows the size scale, pill is fully rounded, square removes the radius, underline strips the box and keeps only a bottom border.

Underline
Rounded
Pill
Square

Sizes

Three sizes (lg, md, sm) scale slot dimensions and type together. sm is the default. When the OTP sits inside a Field, the size is inherited from the Field's size prop.

lg
md
sm

Digit Counts

Set maxLength to the number of characters in the code. The slot count is driven by how many InputOTPSlot children you render, and InputOTPSeparator can be placed anywhere in the group to break a long code into chunks.

4 digits
6 digits
8 digits

States

disabled mutes the slots and blocks input. invalid (read from Field context) lights up the slots with the error color so the user can tell which field failed validation.

Disabled
1
2
3
4
5
6
Invalid
1
2

With Field

Wrap the OTP in Field to attach a label, description, and error. Field's size, invalid, and disabled cascade into the slots, so you don't need to pass them to InputOTP directly.

Verification code

Enter the 6-digit code sent to your phone.

Controlled

Manage the value externally with value and onChange. Use defaultValue for the uncontrolled equivalent. onComplete fires once the user fills every slot.

Current value: (empty)

Accessibility

The underlying input-otp package renders a single hidden <input> that owns the accessible name and keyboard handling. The visible slots are decorative and reflect input state.

KeyDescription
0-9, A-ZTypes a character into the active slot and advances focus.
BackspaceClears the active slot and moves focus back one slot.
Arrow LeftMoves the active slot one position to the left.
Arrow RightMoves the active slot one position to the right.
Tab / Shift+TabMoves focus into or out of the OTP.
Cmd+V / Ctrl+VPastes a code; characters spread across the slots in order.

ARIA notes:

  • Pair every OTP with FieldLabel or pass aria-label to InputOTP so the underlying input has an accessible name.
  • InputOTPSeparator carries role="separator" and is treated as decorative; do not rely on it to convey grouping to assistive tech.
  • The data-state attribute on each slot reflects default, filled, typing, focused, error, or disabled. It exists for styling, not for assistive tech.
  • When Field invalid is set, slots inherit the error state automatically. Pair with FieldError so the failure is also announced as text.

Styling

Tailwind override: pass className to merge Tailwind classes via cn():

<InputOTP className="font-mono" containerClassName="gap-2" />

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="input-otp" on the root OTPInput.
  • data-slot="input-otp-group" on each group wrapper.
  • data-slot="input-otp-slot" on each slot.
  • data-slot="input-otp-separator" on each separator.
  • data-shape, data-size, data-variant, data-invalid, data-disabled on the root.
  • data-state="default | filled | typing | focused | error | disabled" on each slot.

Target a specific slot state in CSS:

[data-slot="input-otp-slot"][data-state="error"] {
  /* … */
}
  • Input: free-form single-line text. Reach for it when length varies or content isn't a fixed-format code.
  • Field: wrap the OTP in Field to attach a label, description, and inline error with size and state cascading automatically.
  • InputGroup: use Input + InputGroup when the value needs a leading or trailing affix (currency prefix, unit suffix).

API Reference

InputOTP

Root of the compound. Wraps the input-otp package's OTPInput, so all of its props (maxLength, value, defaultValue, onChange, onComplete, pattern, pushPasswordManagerStrategy, textAlign, inputMode, etc.) are accepted in addition to the styling props below.

Props

PropTypeDefaultDescription
maxLengthnumber-Total number of characters the input accepts. Required.
valuestring-Controlled value of the OTP.
defaultValuestring-Uncontrolled initial value.
onChange(value: string) => void-Fires on every keystroke with the current concatenated value.
onComplete(value: string) => void-Fires once the value reaches maxLength.
patternstring-Regex that limits the allowed characters (e.g. REGEXP_ONLY_DIGITS).
disabledbooleanfalseBlocks input. Falls back to Field's disabled when wrapped in one.
containerClassNamestring-Classes merged onto the visual container that wraps the slots.
classNamestring-Classes merged onto the underlying hidden input via cn().
childrenReact.ReactNode-The InputOTPGroup / InputOTPSlot / InputOTPSeparator composition.

Variants

VariantOptionsDefaultDescription
shape"rounded" "pill" "square" "underline""rounded"Slot radius and border treatment. underline keeps only a bottom border.
size"lg" "md" "sm""sm"Slot dimensions and type scale. Inherited from Field when wrapped.
variant"outline" "filled""outline"Surface fill. filled adds a subtle background to each slot.

InputOTPGroup

Visual grouping wrapper around a run of slots. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component's classes via cn().
childrenReact.ReactNode-The InputOTPSlot and InputOTPSeparator children.

InputOTPSlot

A single character slot. Extends React.ComponentProps<"div"> and requires an index mapping to the underlying input position. Reads shape, size, variant, disabled, and invalid from the parent InputOTP context, so they aren't repeated as props here.

Props

PropTypeDefaultDescription
indexnumber-Zero-based position of this slot within the OTP value. Required.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().

InputOTPSeparator

Decorative separator between slots. Extends React.ComponentProps<"div"> and sets role="separator". Falls back to a minus icon when no children are passed.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component's classes via cn().
childrenReact.ReactNode-Custom separator content. Defaults to a minus icon.