Skip to content
All Taste components

Inputs

OTP input

Six boxes for a one-time code, with paste, backspace and arrow-key support.

filled · focused · empty · resend timer

Enter the 6-digit code
Sent by SMS to +91 98450 •••21
Didn’t get it?
"use client";
import { Fragment, useRef, useState } from "react";

export function OtpInput({ length = 6, label = "One-time code", onComplete }:
  { length?: number; label?: string; onComplete?: (code: string) => void }) {
  const [digits, setDigits] = useState<string[]>(() => Array(length).fill(""));
  const refs = useRef<(HTMLInputElement | null)[]>([]);
  const focus = (i: number) => refs.current[Math.max(0, Math.min(i, length - 1))]?.focus();

  function fill(from: number, chars: string) {
    const next = [...digits];
    chars.split("").forEach((c, k) => { if (from + k < length) next[from + k] = c; });
    setDigits(next);
    focus(from + chars.length);
    if (next.every(Boolean)) onComplete?.(next.join(""));
  }

  function clear(i: number) {
    const next = [...digits];
    next[i] = "";
    setDigits(next);
  }

  function onKeyDown(i: number, e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === "Backspace" && !digits[i] && i > 0) { e.preventDefault(); clear(i - 1); focus(i - 1); }
    else if (e.key === "ArrowLeft") { e.preventDefault(); focus(i - 1); }
    else if (e.key === "ArrowRight") { e.preventDefault(); focus(i + 1); }
  }

  return (
    <div role="group" aria-label={label} className="flex items-center gap-2">
      {digits.map((d, i) => (
        <Fragment key={i}>
          {i === length / 2 && <span aria-hidden className="h-0.5 w-2 shrink-0 rounded-full bg-border-strong" />}
          <input
            ref={(el) => { refs.current[i] = el; }}
            value={d}
            inputMode="numeric"
            autoComplete={i === 0 ? "one-time-code" : "off"}
            aria-label={"Digit " + (i + 1) + " of " + length}
            onFocus={(e) => e.target.select()}
            onKeyDown={(e) => onKeyDown(i, e)}
            onChange={(e) => {
              const v = e.target.value.replace(/\D/g, "");
              if (v) fill(i, v.slice(-1)); else clear(i);
            }}
            onPaste={(e) => {
              const v = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
              if (v) { e.preventDefault(); fill(0, v); }
            }}
            className="h-[52px] min-w-0 max-w-11 flex-1 basis-0 rounded border border-border-strong bg-bg text-center text-xl font-medium tabular-nums text-fg caret-primary outline-none transition focus:border-primary focus:ring-[3px] focus:ring-primary/20"
          />
        </Fragment>
      ))}
    </div>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.