← Back
UI

OTP Input

Six-digit code input with auto-advance, backspace navigation, paste support, and shake on error.

Enter verification code

Try 123456 to verify

auto-advance · backspace · paste support · keyboard nav

OTPInput.tsx130 lines
"use client";

import { useRef, useState } from "react";

const LENGTH = 6;

export default function OTPInput() {
  const [values, setValues] = useState<string[]>(Array(LENGTH).fill(""));
  const [verified, setVerified] = useState(false);
  const [shake, setShake]   = useState(false);
  const refs = useRef<(HTMLInputElement | null)[]>([]);

  const filled = values.every((v) => v !== "");
  const code = values.join("");

  function focus(i: number) {
    refs.current[i]?.focus();
  }

  function onChange(i: number, raw: string) {
    const char = raw.replace(/\D/g, "").slice(-1);
    const next = [...values];
    next[i] = char;
    setValues(next);
    setVerified(false);
    if (char && i < LENGTH - 1) focus(i + 1);
  }

  function onKeyDown(i: number, e: React.KeyboardEvent) {
    if (e.key === "Backspace") {
      if (values[i]) {
        const next = [...values];
        next[i] = "";
        setValues(next);
      } else if (i > 0) {
        focus(i - 1);
      }
    } else if (e.key === "ArrowLeft" && i > 0) {
      focus(i - 1);
    } else if (e.key === "ArrowRight" && i < LENGTH - 1) {
      focus(i + 1);
    }
  }

  function onPaste(e: React.ClipboardEvent) {
    e.preventDefault();
    const text = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, LENGTH);
    const next = [...Array(LENGTH).fill("")];
    text.split("").forEach((ch, i) => { next[i] = ch; });
    setValues(next);
    focus(Math.min(text.length, LENGTH - 1));
  }

  function verify() {
    if (code === "123456") {
      setVerified(true);
    } else {
      setShake(true);
      setValues(Array(LENGTH).fill(""));
      focus(0);
      setTimeout(() => setShake(false), 500);
    }
  }

  return (
    <div className="flex flex-col items-center justify-center gap-8 py-12 w-full">
      <div className="text-center">
        <p className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Enter verification code</p>
        <p className="text-xs text-zinc-400 dark:text-zinc-600 mt-1">Try <span className="font-mono font-bold text-violet-500">123456</span> to verify</p>
      </div>

      <div
        className="flex gap-3"
        style={{ animation: shake ? "shake 0.4s ease-in-out" : undefined }}
      >
        {values.map((v, i) => (
          <input
            key={i}
            ref={(el) => { refs.current[i] = el; }}
            type="text"
            inputMode="numeric"
            maxLength={1}
            value={v}
            onChange={(e) => onChange(i, e.target.value)}
            onKeyDown={(e) => onKeyDown(i, e)}
            onPaste={onPaste}
            className={`w-11 h-14 text-center text-xl font-bold font-mono rounded-2xl border-2 outline-none transition-all duration-150 bg-white dark:bg-zinc-900 ${
              verified
                ? "border-emerald-400 text-emerald-600 dark:text-emerald-400"
                : shake
                ? "border-red-400 text-red-500"
                : v
                ? "border-violet-500 text-violet-700 dark:text-violet-300"
                : "border-zinc-200 dark:border-zinc-700 text-zinc-900 dark:text-zinc-50 focus:border-violet-400"
            }`}
          />
        ))}
      </div>

      {verified ? (
        <div className="flex items-center gap-2 text-emerald-500 font-semibold text-sm">
          <span className="text-lg">✓</span> Identity verified
        </div>
      ) : (
        <button
          onClick={verify}
          disabled={!filled}
          className="px-8 py-2.5 rounded-full bg-violet-600 text-white text-sm font-semibold disabled:opacity-30 disabled:cursor-not-allowed hover:bg-violet-700 transition-colors"
        >
          Verify
        </button>
      )}

      <p className="text-[10px] font-mono text-zinc-400 dark:text-zinc-600">
        auto-advance · backspace · paste support · keyboard nav
      </p>

      <style>{`
        @keyframes shake {
          0%,100% { transform: translateX(0); }
          20%      { transform: translateX(-8px); }
          40%      { transform: translateX(8px); }
          60%      { transform: translateX(-5px); }
          80%      { transform: translateX(5px); }
        }
      `}</style>
    </div>
  );
}

More in UI