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.
How it works
- Control the value.
valueandonChangekeep the code in React state.onChangegets the new string, which is also used here to clear the error. [1] - 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] - Recover from an error. On a wrong code the value is cleared, the error is shown, and
inputRef.current.focus()puts the cursor back, becauserefpoints 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>
);
}
Type any other six digits to see the error and focus recovery, then type 424242. Code written by AgentGid [1].
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
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.