Skip to main content

Overview

Input OTP provides individual digit slots for entering one-time passwords or verification codes. It handles auto-focus progression between slots, clipboard paste, and backspace deletion automatically.

Installation

npm install @araf-ds/core

Usage

import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
  InputOTPSeparator,
} from "@araf-ds/core"

export default function Example() {
  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>
  )
}

Variants

Basic (6 digits)

<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 (3 + 3)

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

Error State

Invalid code. Please try again.
<div>
  <InputOTP maxLength={6} state="error">
    <InputOTPGroup>
      <InputOTPSlot index={0} />
      <InputOTPSlot index={1} />
      <InputOTPSlot index={2} />
      <InputOTPSlot index={3} />
      <InputOTPSlot index={4} />
      <InputOTPSlot index={5} />
    </InputOTPGroup>
  </InputOTP>
  <p className="text-sm text-red-600 mt-2">Invalid code. Please try again.</p>
</div>

Controlled with Pattern Validation

const [value, setValue] = useState("")

<InputOTP
  maxLength={6}
  value={value}
  onChange={setValue}
  pattern="^[0-9]+$"
>
  <InputOTPGroup>
    <InputOTPSlot index={0} />
    <InputOTPSlot index={1} />
    <InputOTPSlot index={2} />
    <InputOTPSlot index={3} />
    <InputOTPSlot index={4} />
    <InputOTPSlot index={5} />
  </InputOTPGroup>
</InputOTP>

{value.length === 6 && (
  <Button onClick={() => verifyOTP(value)}>Verify</Button>
)}

API Reference

InputOTP

maxLength
number
required
Total number of digit slots.
value
string
Controlled OTP value string.
onChange
(value: string) => void
Callback fired on each character input.
pattern
string
Regex pattern to restrict allowed characters. Default: numeric only ^[0-9]+$.
disabled
boolean
default:"false"
Disables all slots.
state
string
default:"default"
Validation state. Values: default · error
onComplete
(value: string) => void
Callback fired when all slots are filled. Useful for auto-submit.

InputOTPSlot

index
number
required
Zero-based slot index (0 to maxLength - 1).

Accessibility

  • Each slot renders as a single-character <input> with inputmode="numeric" and autocomplete="one-time-code"
  • Focus automatically advances to the next slot on input and retreats on Backspace
  • Pasting a full code fills all slots in one action
  • The group container uses role="group" with an aria-label describing the purpose (e.g. “One-time password”)
  • Error state sets aria-invalid="true" on all slots

Do’s & Don’ts

Do

  • Use onComplete to trigger verification automatically when all slots are filled
  • Support clipboard paste — don’t block paste events
  • Show a resend link with a cooldown timer below the input
  • Use 6 digits for standard OTP, 4 for simpler PIN inputs

Don't

  • Don’t use a regular <Input> for OTP — users expect the slot-by-slot pattern
  • Don’t block numeric keyboard on mobile — use inputmode="numeric"
  • Don’t auto-submit without giving the user a chance to correct mistakes
  • Don’t hide the entered digits — OTP codes are not passwords