Skip to content
Agents tracked: 440 Downloads (7d): 202M down 19.5% GitHub stars: 10.4M Releases (7d): 607 Agent status: 3 with issuesSince last limit reset: Claude 28d · Codex 0d Updated Oct 21, 2026

Auto-submit a code with onComplete

Verification screens usually submit on their own once the full code is in. input-otp's onComplete callback is the hook for that.

All input-otp examples input-otp: usage and stats

Auto-submit a code with onComplete: screenshot of the example below running
The code below, running in Chromium on our server (Oct 12, 2026). Press Run to see it live and change it.

How it works

  1. Control the value. value and onChange keep the code in React state. onChange gets the new string, which is also used here to clear the error. [1]
  2. Submit on completion. onComplete={verify} runs the fake check when the sixth digit lands. The check waits 800 ms, then shows success or resets the field. [2]
  3. Recover from an error. On a wrong code the value is cleared, the error is shown, and inputRef.current.focus() puts the cursor back, because ref points at the real input. [2]

Run and edit it

It runs in your browser, in a sandboxed frame that cannot see this site; the packages load from esm.sh only after you press Run.

import { useRef, useState } from "react";
import { OTPInput, REGEXP_ONLY_DIGITS } from "input-otp";

const CSS = `
.otp-row { display: flex; }
.otp-slot { position: relative; width: 46px; height: 56px; display: flex; align-items: center; justify-content: center;
  border: 1px solid #3a3f4b; border-left-width: 0; background: #1c1f26; color: #f2f4f8;
  font: 600 24px system-ui, sans-serif; outline: 2px solid transparent; transition: outline-color .15s; }
.otp-slot:first-child { border-left-width: 1px; border-radius: 10px 0 0 10px; }
.otp-slot:last-child { border-radius: 0 10px 10px 0; }
.otp-slot.active { outline-color: #6c8cff; z-index: 1; }
.otp-caret { position: absolute; width: 2px; height: 26px; background: #f2f4f8; animation: blink 1s step-end infinite; }
@keyframes blink { 50% { opacity: 0; } }
`;

// One visible box per slot; the real input sits invisibly on top
function Slot({ char, placeholderChar, isActive, hasFakeCaret }) {
  return (
    <div className={isActive ? "otp-slot active" : "otp-slot"}>
      {char ?? placeholderChar}
      {hasFakeCaret && <div className="otp-caret" />}
    </div>
  );
}

const CORRECT_CODE = "424242";

export default function App() {
  const [value, setValue] = useState("");
  const [status, setStatus] = useState("idle"); // idle | pending | success
  const [error, setError] = useState(null);
  const inputRef = useRef(null);

  // Called by onComplete as soon as all six slots are filled
  function verify(code) {
    setStatus("pending");
    setTimeout(() => {
      if (code === CORRECT_CODE) return setStatus("success");
      setStatus("idle");
      setError("That code is incorrect. Try 424242.");
      setValue("");
      setTimeout(() => inputRef.current?.focus(), 0);
    }, 800);
  }

  const msg = error ? [error, "#ff7b7b"] : status === "pending" ? ["Verifying...", "#a9b0c0"]
    : status === "success" ? ["Verified. Signing you in...", "#9fe6a0"] : ["Type 424242 to succeed.", "#a9b0c0"];

  return (
    <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", background: "#111", fontFamily: "system-ui, sans-serif" }}>
      <style>{CSS}</style>
      <div style={{ ...{ background: "#16181d", padding: "26px 32px", borderRadius: 14, color: "#e8eaf0", boxShadow: "0 10px 30px rgba(0,0,0,.5)" }, textAlign: "center" }}>
        <h3 style={{ margin: "0 0 16px", fontSize: 18 }}>Check your phone</h3>
        <OTPInput
          ref={inputRef}
          maxLength={6}
          pattern={REGEXP_ONLY_DIGITS}
          value={value}
          onChange={(next) => { setValue(next); setError(null); }}
          onComplete={verify}
          disabled={status !== "idle"}
          autoFocus
          containerClassName="otp-container"
          render={({ slots }) => (
            <div className="otp-row" style={{ opacity: status === "pending" ? 0.5 : 1 }}>
              {slots.map((slot, idx) => <Slot key={idx} {...slot} />)}
            </div>
          )}
        />
        <p style={{ margin: "16px 0 0", fontSize: 13, minHeight: 18, color: msg[1] }}>{msg[0]}</p>
      </div>
    </div>
  );
}
Ctrl/⌘ + Enter runs

Free for signed-in members, with a daily limit. Describe the change in plain words; the new code runs only in your browser.

Type any other six digits to see the error and focus recovery, then type 424242. Code written by AgentGid [1].

Checked Oct 12, 2026: it starts without errors in Chromium with input-otp 1.5.0, react 19.3.0, react-dom 19.3.0. Needs WebGL.

More input-otp examples

A basic OTP input with input-otp A digits-only PIN in input-otp Two slot groups with a dash in input-otp Alphanumeric codes and paste cleanup in input-otp

Sources

  1. input-otp README (GitHub)
  2. Docs: docs › api

Explanations written by AgentGid from the official docs; the numbers in brackets point here. The code marked "written by AgentGid" is ours and free to copy.

Building something with input-otp? Ask AgentGid →