ThisCalculator

Everyday

React OTP Input: 6-Field Build Guide

Updated · Standard formula, shown below

React OTP Input: 6-Field Build Guide

This guide walks you through building a six-field OTP (one-time password) input in React, the kind you see in banking apps and two-factor authentication flows. You’ll end up with a reusable component where each digit has its own box, focus moves automatically to the next empty field as you type, and pasting a full code fills all six fields at once. You’ll need a working React project (Create React App, Vite, or Next.js all work) and a basic understanding of React hooks and controlled inputs.

What You’ll Need

  • A React project set up with npm or yarn
  • Basic familiarity with React hooks (useState, useRef, useEffect)
  • Understanding of controlled form inputs
  • No external libraries required — this is pure React

Step-by-Step

  1. Create a new component file named OtpInput.jsx (or .tsx if using TypeScript). This keeps the logic isolated and reusable.
  2. Set up the component skeleton with props. The component should accept length (default 6), onComplete (callback when all digits are filled), and optionally value and onChange for controlled usage. Start with a function that returns a container div.
  3. Create state for the OTP values. Use useState to hold an array of strings, one for each input. Initialize as an array of empty strings: const [otp, setOtp] = useState(Array(length).fill('')).
  4. Create refs for each input. Use useRef to store an array of input elements. This allows you to focus specific inputs programmatically. Initialize with useRef([]).
  5. Render the six input fields. Map over the otp array and render an <input> for each. Set maxLength={1}, inputMode="numeric" (to bring up the number pad on mobile), and autoComplete="one-time-code" (helps mobile browsers suggest the code from SMS). Style each input to be square, centered, and with a gap between them.
  6. Handle input changes. Write a function handleChange(e, index) that extracts the typed digit, updates the otp array at that index, and moves focus to the next input if the digit is not empty. Use refs.current[index + 1]?.focus().
  7. Handle backspace navigation. Add an onKeyDown handler. If the key is Backspace and the current field is empty, move focus to the previous input. If it has a value, clear it and stay (or move back after clearing). This makes deleting a sequence feel natural.
  8. Handle paste events. Add an onPaste handler on the container. Extract the pasted text, strip non-digits, and fill the first length characters into the otp array. Then focus the last filled input (or the next empty one).
  9. Add a completion effect. Use useEffect to watch the otp array. If every element is non-empty, join them into a string and call onComplete with that value. Also call onChange if provided.
  10. Handle arrow key navigation (optional but nice). In the onKeyDown handler, also check for ArrowLeft and ArrowRight to move focus accordingly. This improves accessibility.
  11. Style the inputs. Use CSS to make the inputs look like classic OTP boxes: fixed width/height (e.g., 48px), centered text, a bottom border or box shadow, and a focus state that changes the border color. You can add a slight transition for smoothness.
  12. Test the component. In your app, render <OtpInput onComplete={(code) => console.log(code)} />. Try typing, deleting, and pasting a 6-digit code to verify everything works.

Common Mistakes

  • Not clearing the input on backspace when it has a value. Users expect that pressing backspace on a filled field clears it and moves focus back. If you only move focus when empty, the user has to press backspace twice.
  • Using type="number". This can cause unwanted scrollbars on some browsers and allows characters like ‘e’, ‘+’, ‘-‘. Use type="text" with inputMode="numeric" and filter non-digits in the handler.
  • Ignoring the paste event. Many users will paste the code from an SMS or authenticator app. Without a paste handler, the text goes into one field and breaks the input.
  • Not handling the case where the user types a digit in a field that already has a value. If the user clicks a filled field and types a new digit, it should replace the old one, not append. Ensure your handleChange sets the value, not concatenates.
  • Forgetting to set autoComplete="one-time-code". This is a small but important detail for mobile UX. Without it, the browser may not offer to autofill the code from a text message.

Tips

  • Use a single state array rather than six separate states. It’s cleaner and easier to manage for paste and completion logic.
  • Make the component controlled or uncontrolled depending on your needs. If you need to clear the OTP from a parent (e.g., after a failed verification), pass a value prop and reset it. Otherwise, keep it internal.
  • Add a visual focus indicator (like a thicker border) so users know which box is active. This improves usability significantly.
  • Consider mobile keyboard behavior. Using inputMode="numeric" brings up the number pad. Also, set pattern="[0-9]*" to help iOS show the numeric keyboard.
  • Test with screen readers. Add aria-label="Digit 1" or similar to each input for accessibility. Or wrap the container in a fieldset with a legend.

Troubleshooting

  • Focus jumps to the wrong field. Check that your refs array is correctly indexed. The ref callback should be ref={(el) => (refs.current[index] = el)}. If you use a single ref for all, it won’t work.
  • Paste doesn’t fill all fields. Ensure your paste handler reads e.clipboardData.getData('text') and that you call e.preventDefault() to stop the default paste action. Also strip non-digits with .replace(/\D/g, '').
  • Backspace doesn’t move focus. Remember that the onKeyDown fires before the value is cleared. So if you check e.target.value, it still has the old value. Check if the value is empty, and if not, clear it and then move focus.
  • Completion fires prematurely. Make sure you’re checking that all values are non-empty, not just the last one. Use otp.every(v => v !== '').
  • Mobile keyboard doesn’t appear. Double-check that inputMode="numeric" is set. Also, if you’re using type="tel" as an alternative, that works too, but inputMode is more standard.

Frequently Asked Questions

Can I use this with TypeScript?

Yes, the same logic works with TypeScript. You’ll need to type the refs array as useRef<(HTMLInputElement | null)[]>([]) and the OTP state as useState<string[]>(Array(length).fill('')).

How do I clear the OTP input after submission?

If you make the component controlled by passing a value prop and an onChange handler, the parent can reset the value to an empty array. Alternatively, expose a reset method via a ref using useImperativeHandle.

What if I need more than 6 digits?

The component accepts a length prop, so you can render 4, 8, or any number of fields. Just pass length={8} when using it. The logic scales automatically.

How do I handle the Enter key to submit the form?

Add an onKeyDown handler on the last input that checks for Enter and calls onComplete or submits the form. Alternatively, wrap the OTP in a form and use a submit button.

Can I style the inputs to look like boxes with a bottom border only?

Absolutely. Just remove the full border and use border-bottom: 2px solid #ccc. On focus, change the border-bottom color. This is a common design pattern for OTP inputs.

Why does the input allow letters when I type on desktop?

Because inputMode only affects mobile keyboards. On desktop, users can type any character. Your handleChange should filter out non-digits using value.replace(/\D/g, '') before setting state.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.