flohoeller

Otp Input

Component

About

Verification code field with a gliding focus ring, paste and SMS autofill, a calm shake on errors. Follows your shadcn theme, light and dark. From affectUI (affectui.com).

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Otp Input to your project and fits it to your theme.

Code

How Otp Input is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import { useEffect, useRef, useState } from "react";
import { OtpInput, type OtpStatus } from "@/components/ui/otp-input";

const CODE = "482913";
const WRONG = "481973";

const triesText = (n: number) => (n > 0 ? `That code didn't work. ${n} ${n === 1 ? "try" : "tries"} left.` : "That code didn't work. Request a new code.");

export default function Demo() {
  const [value, setValue] = useState("");
  const [status, setStatus] = useState<OtpStatus>("idle");
  const [tries, setTries] = useState(3);
  const timers = useRef<number[]>([]);
  const playing = useRef(true);

  const later = (ms: number, fn: () => void) => timers.current.push(window.setTimeout(fn, ms));
  const stop = () => {
    timers.current.forEach(clearTimeout);
    timers.current = [];
  };

  /** Checks a code like a server would – a short wait, then right or wrong */
  const verify = (code: string) => {
    setStatus("verifying");
    later(1000, () => {
      if (code === CODE) {
        setStatus("success");
        // Back to a fresh field after a while, so it can be tried again
        later(3200, () => {
          setValue("");
          setStatus("idle");
          setTries(3);
        });
      } else {
        setTries((t) => Math.max(0, t - 1));
        setStatus("error");
      }
    });
  };

  // Autoplay once: a wrong code, the shake, then the right one and the morph into "Verified"
  useEffect(() => {
    let t = 700;
    const type = (code: string) => {
      for (let i = 1; i <= code.length; i++) {
        const part = code.slice(0, i);
        later(t, () => setValue(part));
        t += 150;
      }
    };
    type(WRONG);
    later(t + 150, () => verify(WRONG));
    t += 150 + 1000 + 1700;
    // The message folds away as the next try starts
    later(t, () => setStatus("idle"));
    type(CODE);
    later(t + 150, () => {
      playing.current = false;
      verify(CODE);
    });
    return stop;
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // Touching the field ends the autoplay right away
  const takeOver = () => {
    if (!playing.current) return;
    playing.current = false;
    stop();
    setValue("");
    setStatus("idle");
  };

  return (
    <div className="otp-demo" onPointerDownCapture={takeOver} onKeyDownCapture={takeOver} onFocusCapture={takeOver}>
      <style>{demoCss}</style>
      <div className="otp-demo__tray">
        <div className="otp-demo__card">
          <span className="otp-demo__icon" aria-hidden="true">
            <svg viewBox="0 0 24 24">
              <rect x="3.5" y="5.5" width="17" height="13" rx="2.5" />
              <path d="M4.5 7.5l7.5 5.5 7.5-5.5" />
            </svg>
          </span>
          <h3 className="otp-demo__title">Check your email</h3>
          <p className="otp-demo__text">
            We sent a code to <strong>[email protected]</strong>
          </p>
          <OtpInput
            className="otp-demo__otp"
            separator
            align="center"
            value={value}
            status={status}
            errorMessage={triesText(tries)}
            onChange={(code) => {
              setValue(code);
              if (status === "error" && code) setStatus("idle");
            }}
            onComplete={verify}
            onResend={() => setTries(3)}
            resendIn={30}
          />
        </div>
        <p className="otp-demo__hint">
          Demo code: <span>482{"\u00a0"}913</span>
        </p>
      </div>
    </div>
  );
}

const demoCss = `
/* Centered in the preview – the card only grows by the error line, nothing around it moves */
.otp-demo {
  display: grid;
  grid-template-columns: minmax(0, 400px);
  place-content: center;
  width: 100%;
  min-height: 100vh;
  padding: 32px 16px;
  box-sizing: border-box;
  background: var(--color-background, #ffffff);
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
.otp-demo__tray { box-sizing: border-box; min-width: 0; padding: 4px; border-radius: 20px; background: var(--color-muted, #f0f0f0); }
.otp-demo__card {
  display: grid;
  justify-items: center;
  padding: 28px 24px 22px;
  border-radius: 16px;
  background: var(--color-card, #ffffff);
  box-shadow: 0 0 0 1px var(--color-border, #ececec), 0 1px 2px rgba(0, 0, 0, 0.04);
  text-align: center;
}
.otp-demo__icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-bottom: 14px;
  border-radius: 12px;
  background: var(--color-muted, #f4f4f5);
  box-shadow: inset 0 0 0 1px var(--color-border, #ececec);
  color: var(--color-muted-foreground, #8f8f8f);
}
.otp-demo__icon svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.otp-demo__title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--color-foreground, #171717); }
.otp-demo__text { margin: 4px 0 20px; font-size: 13.5px; color: var(--color-muted-foreground, #8f8f8f); overflow-wrap: anywhere; }
.otp-demo__text strong { font-weight: 500; color: var(--color-foreground, #404040); }
.otp-demo__hint { margin: 0; padding: 10px 12px 8px; font-size: 12.5px; text-align: center; color: var(--color-muted-foreground, #8f8f8f); }
.otp-demo__hint span { font-variant-numeric: tabular-nums; color: var(--color-foreground, #404040); }
@media (max-width: 420px) {
  .otp-demo__card { padding: 24px 12px 20px; }
}
`;

More by flohoeller

View all
  • Thinking Loader

    Thinking Loader

    22
  • Message Actions

    Message Actions

    22
  • Email Draft

    Email Draft

  • Tool Call Card

    Tool Call Card

  • Segmented Control

    Segmented Control