AniUI Academy

Build an OTP input

mediumAccessibilityHigh freq+190 XP

Build a 4-digit one-time-passcode input: four separate single-character boxes that read as one value.

Typing a digit in a box should move focus to the next box automatically. Pressing Backspace in a box that is already empty should move focus back to the previous box instead of doing nothing.

Examples

// 4 empty boxes. otp-value = ""

// Focus otp-0, type "1"
// → otp-value = "1", focus jumps to otp-1

// Type "2"
// → otp-value = "12", focus → otp-2

// Press Backspace in an empty otp-2
// → focus jumps back to otp-1

// Type into otp-3 → otp-value = "1234", focus stays on otp-3.

Typing advances; Backspace in an empty box reverses — the two together make four inputs feel like one.

function handleKeyDown(index, e) {
  if (e.key === "Backspace" && values[index] === "" && index > 0) {
    inputRefs[index - 1].current?.focus();
  }
}

Guard with `values[index] === ""` — Backspace in a filled box should delete that char first, then move back on the next press.

What it has to do

  • Four inputs, `data-testid="otp-0"` through `data-testid="otp-3"`, each holding at most one character.
  • A `data-testid="otp-value"` element shows the concatenation of all four boxes as plain text.
  • Typing a character into a box moves focus to the next box (if there is one).
  • Pressing Backspace in an empty box moves focus to the previous box (if there is one) — Backspace in a non-empty box behaves normally otherwise.
  • Typing in the last box does not error or try to move focus past it.

Notes

  • Set `inputMode="numeric"` and `autoComplete="one-time-code"` on each box — iOS/Android surface the SMS code directly as a suggestion.
  • Handle paste on `otp-0`: a user-pasted 4-digit code should fan out across the boxes, not land as "1" in the first box.
  • `maxLength={1}` isn't a hard guarantee on some mobile keyboards — slice to one char in `onChange` to be safe.
  • Keep the array of refs stable across renders — hoist once with `useRef` + fixed length; building a new array in render invalidates focus mid-type.
Asked atRazorpayFlipkartSwiggyStripe

Stuck? The react course covers the ideas this problem is built on.

import { useRef, useState } from "react";

export default function App() {
  const [values, setValues] = useState(["", "", "", ""]);
  const inputRefs = [useRef(null), useRef(null), useRef(null), useRef(null)];

  function handleChange(index, e) {
    const char = e.target.value.slice(-1);
    setValues((current) => {
      const next = [...current];
      next[index] = char;
      return next;
    });
    if (char && index < 3) {
      inputRefs[index + 1].current?.focus();
    }
  }

  function handleKeyDown(index, e) {
    // TODO: pressing Backspace while this box is already empty should
    // move focus to the previous box (index - 1), if there is one.
  }

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <div style={{ display: "flex", gap: 8 }}>
        {values.map((value, index) => (
          <input
            key={index}
            data-testid={`otp-${index}`}
            ref={inputRefs[index]}
            value={value}
            maxLength={1}
            onChange={(e) => handleChange(index, e)}
            onKeyDown={(e) => handleKeyDown(index, e)}
            style={{ width: 32, height: 40, textAlign: "center", fontSize: 20 }}
          />
        ))}
      </div>
      <p>
        Value: <span data-testid="otp-value">{values.join("")}</span>
      </p>
    </div>
  );
}

5 tests · +190 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.