Components

OTP Segmented Input

Six boxes that are secretly one real input - the hard version nobody demos. The input is stretched invisibly over the row (transparent text and caret, never hidden, so autocomplete="one-time-code" SMS autofill and paste just work), and the cells are painted from its value. The active cell is derived from the native selection, never stored beside it: ←/→ move the real caret and the highlight follows, Backspace walks backwards for free, and select-all paints all six cells because a selection range maps to a cell range. Paste 246 810 or 246-810 - one normalize pass strips the junk. Six digits verify on their own: the right code cascades green left to right, a wrong one shakes once, then the digits drop out one by one before the caret comes back. The blueprint toggle tints the hidden input's glyphs red - they are letter-spaced to sit exactly under the cells.

State: idle · 0/6 · attempts 0

Install

npx moumenlab add otp-segmented-input

For AI

Open .md

Usage

"use client";

import OtpInput from "./otp-segmented-input";

// Wire your real check through `verify` — sync or async. Return true and the
// cells cascade green; false and the row shakes, drops the digits, and hands
// the caret back:
//
//   <OtpInput
//     verify={async (code) => {
//       const res = await fetch("/api/verify-otp", { method: "POST", body: code });
//       return res.ok;
//     }}
//   />
export default function OtpInputExample() {
  return (
    <div className="flex flex-col items-center gap-8">
      {/* Default: six digits, verified against your adapter (or the `code` prop). */}
      <OtpInput verify={(code) => code === "246810"} />

      {/* Masked — paints • instead of the digit, the value stays untouched. */}
      <OtpInput mask />

      {/* Split 3-3, the way SMS codes are read aloud. */}
      <OtpInput group />

      {/* Any length — the group gap lands at the halfway point. */}
      <OtpInput length={4} code="2468" group />
    </div>
  );
}