OTP Segmented Input
moumenlab/otp-segmented-inputFreeComponent
Six cells that are secretly one real input, with native selection driving the active cell.
Live preview
live · rendered by the registry
Rendered by moumenlab on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://lab.moumen.dev/r/otp-segmented-input.jsonSource
1"use client";23import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";4import { MotionConfig, motion } from "motion/react";56// OTP segmented input - N boxes, secretly ONE real input.7//8// The version everyone demos is six <input>s wired together with JS focus9// hops. It looks right and behaves wrong: SMS autofill can't fill it (iOS10// offers the code to ONE field), paste needs bespoke splitting, screen11// readers announce six unlabeled boxes, and half the keyboard is re-invented.12//13// This is the hard version: one real <input> stretched invisibly over the14// whole row (color and caret transparent - NOT display:none, it must stay15// focusable and autofillable), with the cells painted underneath from its16// value. Everything hard becomes free:17//18// · SMS autofill just works - autocomplete="one-time-code" on a real,19// visible-to-the-browser input.20// · Paste just works - "246 810" lands in the input, one normalize pass21// strips the junk, the cells repaint.22// · Backspace walks backwards and ←/→ move the caret because they are the23// NATIVE caret - the active cell is derived from selectionStart, never24// stored beside it. Select-all paints all cells selected, because a25// selection range maps to a cell range.26// · The input's own glyphs are letter-spaced to sit under the cells, so the27// blueprint toggle can simply tint them red and you SEE the real input28// lying over the fake one.29//30// Verification is yours: pass `verify` (sync or async - hit your API) and a31// full code drives the little state machine: right → the cells cascade green32// left to right; wrong → the row shakes, the digits drop out one by one, then33// the field clears and hands the caret back. Without `verify` it compares34// against the `code` prop, so the component demos out of the box.35//36// Animation via motion/react; honours prefers-reduced-motion. Requires the37// lab-theme tokens. Fully Tailwind, no CSS files.3839const EASE = [0.22, 1, 0.36, 1] as const;40const SHAKE_S = 0.38; // wrong code: the row shake41const DROP_S = 0.24; // each digit's fall-out42const STAGGER_S = 0.045; // per-digit clear offset43const FILL_S = 0.055; // per-cell success cascade offset4445const OTP_VARS = {46 "--otp-cell-w": "2.75rem",47 "--otp-cell-h": "3.25rem",48 "--otp-gap": "0.5rem",49} as CSSProperties;5051export interface OtpInputState {52 length: number;53 caret: { start: number; end: number };54 state: "idle" | "success" | "error";55// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from moumenlab
All 14 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Caret-Anchored Mention Popovercaret-mention-popover | moumenlab | Components | Free | 1 dep | |
| Command Palette with Argument Chipscommand-palette | moumenlab | Components | Free | 1 dep | |
| Drag-to-Reorder Listdrag-to-reorder-list | moumenlab | Components | Free | 1 dep | |
| File Upload Staging Areafile-upload-staging | moumenlab | Components | Free | 1 dep | |
| Hover-Expand Icon Striphover-expand-icon-strip | moumenlab | Components | Free | 1 dep | |
| Inertial Wheel Listinertial-wheel-list | moumenlab | Components | Free | 1 dep | |
| Morphing Checkout Flowmorphing-checkout | moumenlab | Components | Free | 1 dep | |
| Schedule Builderschedule-builder | moumenlab | Components | Free | 1 dep |
