Back to Dashboard
Machine CodingINTERMEDIATE

Implement an OTP Input component with auto-focus and backspace handling.

ReactDOMUX

Live Interactive Demo

Loading Demo...

Implementation

typescript.tsx
import React, { useState, useRef, KeyboardEvent, ChangeEvent, ClipboardEvent } from "react";

const OTPInput = ({ length = 6, onComplete = (val: string) => console.log(val) }) => {
  const [otp, setOtp] = useState<string[]>(new __PH_60__(length).fill(""));
  const inputRefs = useRef<(HTMLInputElement | null)[]>([]);

  const focusInput = (index: number) => {
    if (index >= 0 && index < length && inputRefs.current[index]) {
      inputRefs.current[index]?.focus();
    }
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
    if (isNaN(Number(e.target.value))) return;
    const newOtp = [...otp];
    const val = e.target.value.slice(-1);
    newOtp[index] = val;
    setOtp(newOtp);
    if (newOtp.every(v => v !== "")) onComplete(newOtp.join(""));
    if (val && index < length - 1) focusInput(index + 1);
  };

  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
    if (e.key === "Backspace") {
      if (!otp[index] && index > 0) {
        const newOtp = [...otp];
        newOtp[index - 1] = "";
        setOtp(newOtp);
        focusInput(index - 1);
      } else {
        const newOtp = [...otp];
        newOtp[index] = "";
        setOtp(newOtp);
      }
    } else if (e.key === "ArrowLeft" && index > 0) focusInput(index - 1);
    else if (e.key === "ArrowRight" && index < length - 1) focusInput(index + 1);
  };

  const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
    e.preventDefault();
    const pasteData = e.clipboardData.getData("text").slice(0, length);
    if (!/^\d+$/.test(pasteData)) return;
    const newOtp = [...otp];
    pasteData.split("").forEach((char, i) => newOtp[i] = char);
    setOtp(newOtp);
    const nextIndex = pasteData.length < length ? pasteData.length : length - 1;
    focusInput(nextIndex);
    if (newOtp.every(v => v !== "")) onComplete(newOtp.join(""));
  };

  return (
    <div className="flex gap-2 justify-center p-4">
      {otp.map((data, index) => (
        <input
          key={index}
          type="text"
          inputMode="numeric"
          ref={(el) => { inputRefs.current[index] = el; }}
          value={data}
          onChange={(e) => handleChange(e, index)}
          onKeyDown={(e) => handleKeyDown(e, index)}
          onPaste={handlePaste}
          onFocus={(e) => e.target.select()}
          className="w-12 h-14 text-center text-2xl font-bold border-2 rounded outline-none"
        />
      ))}
    </div>
  );
};

The Core Concept

A professional OTP input requires seamless focus management. Keys include:

1. **Ref Arrays**: dynamic refs to access each input element. 2. **onChange**: moving focus to the next input when a digit is entered. 3. **onKeyDown**: handling Backspace to clear and move focus to the previous input. 4. **Paste functionality**: splitting a pasted string across all inputs.