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>
  );
}

Story

  1. An experiment on one moment

    This one was pure experiment. OTP inputs are everywhere, so I picked that one small, universal moment, typing a six-digit code, and asked how good the interaction could get. The whole component really exists for the two seconds after the last digit lands.

  2. Designed for eyes that are elsewhere

    The detail that drove everything: when a code arrives, you are looking at your phone, not the screen, and sometimes the phone is across the desk. So the result can't be a toast in a corner. Right or wrong has to read from a distance, in color and motion big enough to catch you even when your eyes are elsewhere.

  3. The six-inputs trap

    The version everyone demos is six inputs wired together with JS focus hops. It looks right and behaves wrong: iOS offers SMS autofill to one field, so it can't fill six; paste needs bespoke splitting; screen readers announce six unlabeled boxes; half the keyboard gets re-invented.

  4. Secretly one input

    So this build stretches one real input invisibly over the whole row and paints the cells underneath from its value. Everything hard becomes free: SMS autofill just works, paste just works, and backspace and arrows are the native caret, not a re-implementation. Professional starts with "it works".

  5. The animation challenge

    Animations are normally not good all the time, especially on professional sites, where they read as toys. The challenge was motion that is easy to understand and professional at once. The rule that survived: every movement must state a fact. Anything decorative got cut.

  6. Two endings, both unmistakable

    Right: the cells cascade green left to right, one cell at a time. Wrong: the row shakes, the digits drop out one by one, then the field clears and hands the caret back so the retry starts instantly. You can tell which ending happened from across the desk, without reading a word.

References