AniUI

InputOTP

A one-time password input component with auto-focus and backspace navigation.

Enter verification code

We sent a code to your email

<InputOTP length={6} value={otp} onValueChange={setOtp} />

Installation

npx @aniui/cli add input-otp

Usage

app/index.tsx
import { InputOTP } from "@/components/ui/input-otp";
import { useState } from "react";

export function MyScreen() {
  const [otp, setOtp] = useState("");
  return (
    <InputOTP
      length={6}
      value={otp}
      onValueChange={setOtp}
    />
  );
}

Examples

6-digit verification code

<InputOTP length={6} value={otp} onValueChange={setOtp} />

4-digit PIN

<InputOTP length={4} value={pin} onValueChange={setPin} />

Props

PropTypeDefault
length
number
6
value
string
""
onValueChange
(value: string) => void
className
string

Also accepts all View props from React Native.

Source

components/ui/input-otp.tsx
import React, { useRef, useState } from "react";
import { View, TextInput } from "react-native";
import { cn } from "@/lib/utils";

export interface InputOTPProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  length?: number;
  value?: string;
  onValueChange?: (value: string) => void;
}
export function InputOTP({ length = 6, value = "", onValueChange, className, ...props }: InputOTPProps) {
  const refs = useRef<(TextInput | null)[]>([]);
  const [focused, setFocused] = useState(0);
  const digits = value.padEnd(length, "").slice(0, length).split("");
  const handleChange = (text: string, index: number) => {
    const char = text.slice(-1);
    const next = [...digits];
    next[index] = char;
    onValueChange?.(next.join(""));
    if (char && index < length - 1) {
      refs.current[index + 1]?.focus();
    }
  };
  const handleKeyPress = (key: string, index: number) => {
    if (key === "Backspace" && !digits[index] && index > 0) {
      refs.current[index - 1]?.focus();
    }
  };
  return (
    <View className={cn("flex-row gap-2", className)} accessibilityRole="none" {...props}>
      {digits.map((digit, i) => (
        <TextInput
          key={i}
          ref={(el) => { refs.current[i] = el; }}
          className={cn(
            "h-12 w-10 rounded-md border text-center text-lg font-semibold text-foreground bg-background",
            focused === i ? "border-ring" : "border-input"
          )}
          value={digit}
          onChangeText={(t) => handleChange(t, i)}
          onKeyPress={({ nativeEvent }) => handleKeyPress(nativeEvent.key, i)}
          onFocus={() => setFocused(i)}
          keyboardType="number-pad"
          maxLength={1}
          selectTextOnFocus
          accessible={true}
          accessibilityLabel={`Digit ${i + 1} of ${length}`}
        />
      ))}
    </View>
  );
}