How to Think About This Problem
An OTP component looks small, but it is a focus-management problem. The user expects it to behave like one input, even though the UI shows six boxes.
The senior approach is to keep one source of truth for the code, then make focus movement feel natural. The code is data. Focus is behavior.
State Model
Use an array of six strings:
const [digits, setDigits] = useState(Array(6).fill(''));
const code = digits.join('');
This is easier than six separate state variables. Paste, delete, validation, and reset all become simple array operations.
Refs for Focus
To move focus between boxes, store input nodes in an array ref:
const refs = useRef([]);
refs.current[index + 1]?.focus();
refs.current[index - 1]?.focus();
This is okay because focus is an imperative browser action. You are not using refs to hide state from React. You are using them to call the DOM focus API.
Key Behaviors
- Typing: keep only the last numeric character and move to the next box.
- Backspace: if the current box is empty, move to the previous box.
- Arrow keys: move left and right without changing digits.
- Paste: sanitize the pasted value and distribute digits from the current box.
- Verify: validate the derived
code, not each input separately.
Paste Handling
Paste is the edge case that separates a polished OTP input from a basic one. Users often copy the whole code from SMS or email. If they paste into the first box, all boxes should fill.
const pasted = text.replace(/\D/g, '').slice(0, 6 - index);
setDigits(current => {
const next = [...current];
pasted.split('').forEach((char, offset) => {
next[index + offset] = char;
});
return next;
});
Common Pitfall: Treating each box as a separate form fieldThat makes the component harder than it needs to be. The product cares about one code, not six unrelated values.
Mobile Details
Use inputMode="numeric" so mobile devices show the numeric keyboard. Use autoComplete="one-time-code" on the first input so supported browsers can suggest SMS codes.
Edge Cases
- Paste values with spaces or dashes, like
492-817.
- Delete from the middle and continue typing.
- Submit before all six digits are present.
- Show invalid-code feedback and reset focus to the first box.
- Disable resend until the timer finishes.
Interview TipSay: "I want the six boxes to behave like one logical input. The array stores the code, and refs only handle focus movement."
Remember: For OTP Verification Input: nail the state model before JSX — Controlled state, Focus handling, Paste support.
Say this in the interview: I would clarify interactions for OTP Verification Input, model state and derived UI first, then build components around that invariant.