rapidUI

Input OTP

One-time-password input with individual slots, built on input-otp.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/input-otp.json

Installs to @/components/ui/input-otp.

Manual installation

Install the dependencies:

pnpm add input-otp@^1.5.0 lucide-react@^1.48.0

Copy the source into your project:

components/ui/input-otp.tsx
'use client'

import { OTPInput, OTPInputContext } from 'input-otp'
import { MinusIcon } from 'lucide-react'
import * as React from 'react'
import { cn } from '@/lib/utils'

function InputOTP({
	className,
	containerClassName,
	...props
}: React.ComponentProps<typeof OTPInput> & {
	containerClassName?: string
}) {
	return (
		<OTPInput
			data-slot="input-otp"
			containerClassName={cn('cn-input-otp flex items-center has-disabled:opacity-50', containerClassName)}
			spellCheck={false}
			className={cn('disabled:cursor-not-allowed', className)}
			{...props}
		/>
	)
}

function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
	return (
		<div
			data-slot="input-otp-group"
			className={cn(
				'flex items-center rounded-lg has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40',
				className,
			)}
			{...props}
		/>
	)
}

function InputOTPSlot({
	index,
	className,
	...props
}: React.ComponentProps<'div'> & {
	index: number
}) {
	const inputOTPContext = React.useContext(OTPInputContext)
	const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}

	return (
		<div
			data-slot="input-otp-slot"
			data-active={isActive}
			className={cn(
				'relative flex size-10 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-3 data-[active=true]:ring-ring data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40',
				className,
			)}
			{...props}
		>
			{char}
			{hasFakeCaret && (
				<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
					<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
				</div>
			)}
		</div>
	)
}

function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
	return (
		<div
			data-slot="input-otp-separator"
			className="flex items-center [&_svg:not([class*='size-'])]:size-4"
			role="separator"
			{...props}
		>
			<MinusIcon />
		</div>
	)
}

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

An accessible OTP (One-Time Password) input component with individual character slots and visual feedback. Perfect for verification codes, PIN entries, and secure authentication flows.

Features

  • Individual character slots with visual focus indicators
  • Animated caret for active slot feedback
  • Composable slot grouping with separators
  • Pattern validation support (digits only, letters only, alphanumeric, custom patterns)
  • Controlled and uncontrolled state management
  • Disabled and invalid (aria-invalid) states
  • Auto-focus and keyboard navigation
  • Copy/paste support
  • Fully accessible with proper ARIA attributes
  • Built on top of the input-otp library

Usage

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

// Basic usage — each slot reads its state from context by `index`
<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
<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>

// Digits only
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS}>...</InputOTP>

// Controlled
const [value, setValue] = useState('')
<InputOTP maxLength={6} value={value} onChange={setValue}>...</InputOTP>

Examples

Default

An email confirmation step: six digits split 3-3 with a separator, labelled with FieldLabel via the id on InputOTP.

With Separator

Visual separator between two groups of slots.

Multiple Separators

Pairs of digits, here for a date of birth.

Patterns

pattern restricts which characters are accepted: digits only, letters only, or digits and letters.

Controlled

Controlled OTP input with external state management.

Disabled

OTP input in disabled state with a pre-filled value.

Invalid

Set aria-invalid on the slots to show the error state, and describe the error with FieldError.

Custom Length

A 4-digit PIN.

With Completion

onComplete fires when all slots are filled — here it signs an application and shows the result.

Props

InputOTP (Root)

  • maxLength: number - Maximum number of characters (required)
  • value?: string - The controlled value of the input
  • onChange?: (value: string) => void - Callback fired when value changes
  • onComplete?: (value: string) => void - Callback fired when all slots are filled
  • pattern?: string - Regex source string to validate input characters
  • disabled?: boolean - Whether the input is disabled
  • autoFocus?: boolean - Whether to auto-focus the first slot
  • containerClassName?: string - CSS class for the container element
  • children - InputOTPGroup / InputOTPSlot / InputOTPSeparator elements (or use the lower-level render prop of input-otp)

InputOTPGroup

  • Extends standard div props
  • Used to group related slots together
  • Provides consistent layout and shows the error ring when a slot has aria-invalid

InputOTPSlot

  • index: number - The slot index (required). The character, active state and caret are read from the InputOTP context.
  • aria-invalid - Shows the destructive error state
  • Extends standard div props

InputOTPSeparator

  • Extends standard div props (role="separator")
  • Renders a minus icon between groups
  • Can be customized with CSS classes for styling

Patterns

The component supports common input patterns:

import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'

// Numbers only (0-9)
<InputOTP pattern={REGEXP_ONLY_DIGITS} />

// Letters only (a-z, A-Z)
<InputOTP pattern={REGEXP_ONLY_CHARS} />

// Digits and letters
<InputOTP pattern={REGEXP_ONLY_DIGITS_AND_CHARS} />

// Custom pattern
<InputOTP pattern="^[a-fA-F0-9]+$" />

Accessibility

The Input OTP component follows WCAG 2.1 guidelines and provides:

  • Proper ARIA labels and descriptions
  • Keyboard navigation support:
    • ←/→: Navigate between slots
    • Backspace: Delete character and move to previous slot
    • Delete: Delete character in current slot
    • Home/End: Move to first/last slot
    • Ctrl+A: Select all text
    • Ctrl+V: Paste clipboard content
  • Focus management with visual indicators
  • Screen reader announcements for value changes
  • Support for disabled states
  • High contrast visual feedback

Best Practices

  1. Length and Grouping

    • Use appropriate maxLength based on your use case (4-8 characters common)
    • Group related digits with InputOTPGroup for better visual organization
    • Use separators to break up longer codes (e.g., 3-3 for 6-digit codes)
  2. Validation

    • Use pattern validation to restrict input types (digits, letters, alphanumeric)
    • Provide clear feedback about expected input format
    • Validate on completion rather than per-character for better UX
  3. User Experience

    • Enable auto-focus for immediate input capability
    • Provide clear instructions about expected code format
    • Show loading states during verification
    • Handle paste operations gracefully
  4. Security

    • Clear sensitive values after use
    • Consider auto-submit on completion for better flow
    • Implement rate limiting on the backend
    • Use HTTPS for all OTP-related communications
  5. Error Handling

    • Show clear error messages for invalid codes
    • Allow users to easily clear and re-enter codes
    • Provide resend functionality with appropriate cooldown
    • Handle network errors gracefully

Common Use Cases

  • Two-Factor Authentication: 6-digit TOTP codes
  • SMS Verification: 4-6 digit SMS codes
  • PIN Entry: 4-digit PINs for secure access
  • Backup Codes: 8-character alphanumeric recovery codes
  • Email Verification: Variable length verification codes
  • Input
  • Form
  • Label
  • Button