Why OTP Input Is a Popular Machine Coding Question
The OTP input is one of the most common frontend machine coding round questions because it looks simple on the surface but tests multiple frontend engineering skills at once. An interviewer can quickly see how a candidate thinks about state design, keyboard events, focus management, controlled inputs, edge cases, accessibility, and user experience.
A weak implementation only renders six boxes and stores values. A strong implementation handles typing, deletion, pasting, arrow-key navigation, invalid input, mobile-friendly behavior, and clean component design. That difference is exactly what machine coding rounds are meant to reveal.
What the Interviewer Usually Expects
- Render a fixed number of OTP input boxes, such as 4 or 6.
- Allow only one digit per box.
- Move focus to the next box after entering a digit.
- Move focus backward when backspace is pressed on an empty box.
- Support pasting a full OTP at once.
- Keep the implementation clean, maintainable, and predictable.
Some interviewers may also ask for arrow-key navigation, auto-submit when all digits are filled, masking, validation messages, or support for alphanumeric OTP codes. The base problem is the same, but these additions help them judge how well the candidate can extend a working solution.
How to Think About the Problem Before Writing Code
A common mistake in machine coding rounds is jumping into the JSX immediately. A better approach is to break the problem into responsibilities. First, decide how the OTP value will be stored. Then define how typing should update state. After that, plan how focus should move. Finally, think about edge cases such as paste behavior and deletion.
The simplest mental model is to treat the OTP as an array of characters. If the OTP length is 6, then the state can be something like ['1', '2', '', '', '', '']. Each input box maps to one index in that array. This model is easy to render, update, validate, and combine into a final OTP string.
Visualizing the Component
Imagine a login screen where the user receives a 6-digit code on their phone. The interface shows six small input boxes in a row. When the user types 5 in the first box, the cursor automatically jumps to the second box. If they paste 483921, all six boxes fill instantly. If they press backspace on an empty third box, focus moves back to the second box. This is the expected experience.
Step 1: State Design
The component needs a predictable representation of user input. The best choice is an array with one entry per OTP digit. This makes rendering straightforward because each input maps naturally to an index.
const OTP_LENGTH = 6;
const [otp, setOtp] = useState<string[]>(
new Array(OTP_LENGTH).fill("")
);This is more flexible than storing a single string during editing because each box has its own value and update rules. Once all digits are collected, the array can be joined into a single OTP string when needed.
Step 2: Rendering Input Boxes
Each OTP cell is a controlled input. The array drives the UI, so there is a single source of truth. That makes the component easier to debug and reason about during the interview.
{
otp.map((digit, index) => (
<input
key={index}
value={digit}
maxLength={1}
/>
));
}At this stage, the boxes render correctly, but they do not yet support focus movement or validation. That behavior comes next.
Step 3: Handling Input Changes
When the user types into one input box, only the matching index in the OTP array should update. The input should also reject invalid characters if the requirement is digits only.
const handleChange = (value: string, index: number) => {
if (!/^[0-9]?$/.test(value)) return;
const nextOtp = [...otp];
nextOtp[index] = value;
setOtp(nextOtp);
};The regular expression allows either one digit or an empty string. This prevents letters and multi-character values from entering state. In a machine coding round, small validation decisions like this show attention to detail.
Step 4: Auto-Moving Focus Forward
A polished OTP input moves the cursor to the next box automatically after a valid digit is entered. To do that, the component needs access to all input elements. A common way is to store references in an array.
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
const handleChange = (value: string, index: number) => {
if (!/^[0-9]?$/.test(value)) return;
const nextOtp = [...otp];
nextOtp[index] = value;
setOtp(nextOtp);
if (value && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}
};This creates the expected OTP experience. The user does not need to manually click the next box after every digit.
Step 5: Handling Backspace Properly
Backspace behavior is where many candidates lose polish. If the current box has a value, backspace should clear it. If the current box is already empty, backspace should move focus to the previous box so the user can continue deleting naturally.
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number
) => {
if (e.key === "Backspace") {
if (otp[index]) {
const nextOtp = [...otp];
nextOtp[index] = "";
setOtp(nextOtp);
return;
}
if (index > 0) {
inputRefs.current[index - 1]?.focus();
const nextOtp = [...otp];
nextOtp[index - 1] = "";
setOtp(nextOtp);
}
}
};This small interaction detail makes the component feel natural and well thought out. Interviewers often check this behavior explicitly.
Step 6: Supporting Paste
Paste support is one of the strongest signals of implementation maturity. Real users often paste the entire OTP instead of typing digit by digit. A good OTP component should distribute pasted characters across input boxes automatically.
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>
) => {
e.preventDefault();
const pastedData = e.clipboardData
.getData("text")
.trim()
.slice(0, OTP_LENGTH);
if (!/^\d+$/.test(pastedData)) return;
const nextOtp = pastedData.split("");
while (nextOtp.length < OTP_LENGTH) {
nextOtp.push("");
}
setOtp(nextOtp);
const lastFilledIndex = Math.min(pastedData.length, OTP_LENGTH) - 1;
inputRefs.current[lastFilledIndex]?.focus();
};For example, if the user pastes 4821 into a 6-digit OTP input, the state becomes ['4', '8', '2', '1', '', '']. The first four boxes fill immediately, and focus can move to the fourth or next empty box depending on the preferred behavior.
Step 7: Arrow-Key Navigation
Arrow-key support is not always required, but it improves usability and demonstrates strong keyboard interaction handling. Left arrow should move to the previous input, and right arrow should move to the next one.
if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowRight" && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}This is the kind of refinement that helps an implementation stand out in a competitive frontend round.
Complete Example Component
import React, { useRef, useState } from "react";
const OTP_LENGTH = 6;
export default function OTPInput() {
const [otp, setOtp] = useState<string[]>(
new Array(OTP_LENGTH).fill("")
);
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
const handleChange = (value: string, index: number) => {
if (!/^[0-9]?$/.test(value)) return;
const nextOtp = [...otp];
nextOtp[index] = value;
setOtp(nextOtp);
if (value && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number
) => {
if (e.key === "Backspace") {
if (otp[index]) {
const nextOtp = [...otp];
nextOtp[index] = "";
setOtp(nextOtp);
return;
}
if (index > 0) {
inputRefs.current[index - 1]?.focus();
const nextOtp = [...otp];
nextOtp[index - 1] = "";
setOtp(nextOtp);
}
}
if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowRight" && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>
) => {
e.preventDefault();
const pastedData = e.clipboardData
.getData("text")
.trim()
.slice(0, OTP_LENGTH);
if (!/^\d+$/.test(pastedData)) return;
const nextOtp = pastedData.split("");
while (nextOtp.length < OTP_LENGTH) {
nextOtp.push("");
}
setOtp(nextOtp);
const focusIndex = Math.min(pastedData.length, OTP_LENGTH) - 1;
inputRefs.current[focusIndex]?.focus();
};
const otpValue = otp.join("");
return (
<div>
<div style={{ display: "flex", gap: "8px" }}>
{otp.map((digit, index) => (
<input
key={index}
ref={(node) => {
inputRefs.current[index] = node;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
style={{
width: "40px",
height: "40px",
textAlign: "center",
fontSize: "18px",
}}
/>
))}
</div>
<p>Entered OTP: {otpValue}</p>
</div>
);
}How the User Experience Flows
Suppose the user types 3 in the first box. The state becomes ['3', '', '', '', '', ''], and focus shifts to the second box. Then they type 7, so state becomes ['3', '7', '', '', '', '']. If they realize the second digit is wrong and press backspace, the second box clears. If they press backspace again, focus moves back to the first box. If instead they paste 384291 into the first box, the entire OTP fills in one action.
This kind of clear mental walkthrough is valuable in interviews because it shows the logic is intentional, not accidental.
Common Mistakes Candidates Make
- Using uncontrolled inputs and then struggling to sync state.
- Not handling backspace on empty inputs.
- Ignoring paste support entirely.
- Allowing non-digit characters.
- Hardcoding logic in a way that only works for 4 digits or 6 digits.
- Writing messy focus movement logic that breaks at boundaries.
A machine coding round is not just about making something work once. It is about making the behavior predictable, reusable, and clean under realistic interactions.
How to Make the Component More Reusable
A stronger production-style solution turns the OTP length into a prop and exposes the full OTP through a callback such as onComplete or onChange. That way, the same component can be reused for 4-digit login OTPs, 6-digit verification codes, or even alphanumeric confirmation codes with small modifications.
type OTPInputProps = {
length?: number;
onComplete?: (otp: string) => void;
};This shows the interviewer that the candidate is not only solving the problem but also thinking in terms of component design and extensibility.
Accessibility Considerations
Frontend interviews increasingly reward candidates who consider accessibility. Each input should have a meaningful label or aria-label, focus should remain visible, and keyboard-only users should be able to complete the OTP without using a mouse. On mobile devices, using inputMode='numeric' improves the keyboard experience significantly.
A polished OTP component is not just visually correct. It should also be usable by as many users as possible.
What This Question Actually Tests
Although the problem statement sounds like a UI task, it actually tests multiple layers of frontend engineering. It checks whether the candidate can model state cleanly, write event-driven logic, control focus, reason about UX edge cases, and structure a component that remains understandable under pressure.
That is why OTP input continues to appear in machine coding rounds. It is small enough to build in one session, but rich enough to separate average implementations from strong ones.
Final Takeaway
Building an OTP input feature in a frontend machine coding round is not about drawing six boxes. It is about designing a smooth interaction model. A complete solution handles typing, deletion, focus movement, paste behavior, validation, and keyboard navigation in a way that feels natural to the user.
The strongest interview solutions usually come from candidates who first think about state and flow, then implement the UI as a reflection of that logic. When done well, the OTP component becomes a compact demonstration of solid frontend engineering.