Frontend
easy
mid
Build an OTP component
An array of N single-char inputs. Key behaviors: auto-advance focus on entry, backspace moves to the previous box, paste fills all boxes at once, accept only digits, expose the joined value. Manage refs in an array, keep state as a string[].
5 min read·~25 min to think through
An OTP input looks trivial — N tiny boxes — but interviewers are testing focus management, paste handling, and keyboard UX.
Core data model
jsx
function OTPInput({ length = 6, onComplete }) {
const [values, setValues] = useState(Array(length).fill(""));
const refs = useRef([]);
const focusIndex = (i) => refs.current[i]?.focus();
const handleChange = (i, e) => {
const digit = e.target.value.replace(/\D/g, "").slice(-1); // keep last digit only
if (!digit) return;
const next = [...values];
next[i] = digit;
setValues(next);
if (i < length - 1) focusIndex(i + 1); // auto-advance
if (next.every(Boolean)) onComplete?.(next.join(""));
};
const handleKeyDown = (i, e) => {
if (e.key === "Backspace" && !values[i] && i > 0) {
focusIndex(i - 1); // backspace on empty → go back
}
if (e.key === "ArrowLeft" && i > 0) focusIndex(i - 1);
if (e.key === "ArrowRight" && i < length - 1) focusIndex(i + 1);
};
const handlePaste = (e) => {
const digits = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
if (!digits) return;
e.preventDefault();
const next = Array(length).fill("");
[...digits].forEach((d, idx) => (next[idx] = d));
setValues(next);
focusIndex(Math.min(digits.length, length - 1));
if (next.every(Boolean)) onComplete?.(next.join(""));
};
return (
<div onPaste={handlePaste}>
{values.map((v, i) => (
<input
key={i}
ref={(el) => (refs.current[i] = el)}
value={v}
onChange={(e) => handleChange(i, e)}
onKeyDown={(e) => handleKeyDown(i, e)}
inputMode="numeric"
maxLength={1}
aria-label={`Digit ${i + 1}`}
/>
))}
</div>
);
}What interviewers grade
- Auto-advance — typing moves focus forward; the most expected behavior.
- Backspace on empty — moves to the previous box (and clears it). Without this, the component feels broken.
- Paste — users paste the whole code from an SMS. Handle
onPaste, distribute digits, focus the right box. - Input sanitization — strip non-digits;
inputMode="numeric"brings up the numeric keypad on mobile. - Refs in an array —
refs.current[i]set via the callback ref pattern. - Accessibility —
aria-labelper box, autocompleteone-time-codeso iOS autofills.
The framing
"I model it as a string[] of length N with a parallel array of refs. The four behaviors that make it feel real: auto-advance on type, backspace-on-empty to go back, full paste distribution, and digit-only sanitization. The trap is treating each box as independent — focus management is the whole problem."
Follow-up questions
- •How do you handle pasting a 6-digit code into the first box?
- •How would you support autocomplete='one-time-code' for iOS SMS autofill?
- •How do you make this accessible to screen readers?
- •How would you add a countdown timer and resend button?
Common mistakes
- •Forgetting paste handling — users paste codes from SMS.
- •Backspace doesn't move focus back, so correcting a digit is painful.
- •Not sanitizing input — letters or multiple chars land in a box.
- •Storing refs incorrectly instead of an array indexed by position.
- •No onComplete callback, so the parent can't react when all boxes are filled.
Performance considerations
- •Trivially cheap — N is small (4–6). The only concern is avoiding re-creating the refs array on every render; useRef holds it stably.
Edge cases
- •Pasting more digits than boxes — slice to length.
- •Pasting fewer digits — fill what you can, focus the next empty box.
- •Mobile keyboards inserting non-digit characters.
- •User clicks into the middle box first — handle out-of-order entry.
Real-world examples
- •2FA/login code entry in banking and auth flows.
- •Email verification code screens.
Senior engineer discussion
Seniors treat it as a focus-management problem, not N independent inputs. They handle paste first (the most-missed requirement), wire backspace-on-empty, sanitize input, add autoComplete='one-time-code', and expose a clean onComplete contract to the parent.
Related questions
Frontend
Easy
6 min
Frontend
Medium
4 min