Micro-interactions

Passcode

A one-time-code field whose cells answer every keystroke: each character lands compressed and springs back, a paste distributes across the row rather than filling it on one frame, a wrong code shakes and clears itself, and a right one folds the whole row into a tick. Underneath there is exactly one real input holding the whole code — which is what keeps SMS autofill, password managers, select-all and a screen reader working, none of which survive six inputs with maxLength one.

Install

bun add motion clsx tailwind-merge

Then copy 2 files into your project:

  • components/registry/passcode/passcode.tsx
  • lib/utils.ts

Props

PropTypeDefaultDescription
lengthnumber6Cells in the code. Six is the industry default because it is about the longest string most people can hold in working memory between reading it and typing it — the control exists so you can feel where that stops being true.
mode"numeric" | "alphanumeric""numeric"What the field accepts. Numeric also asks for the number pad, which matters far more on a phone than the validation does — an alphanumeric code field that opens a full keyboard costs the user two taps every single time.
maskbooleanfalseRender dots instead of characters. Off by default on purpose: a one-time code is worthless a minute after it arrives, and masking it only stops the person entitled to read it from checking their own typing.
stiffnessnumber520Spring tension of the per-character pop. High enough to land inside the gap between two fast keystrokes, or the cells queue up behind a quick typist and the feedback arrives after the character it is confirming.
dampingnumber26Spring friction. Low rings past and settles, which is the difference between a cell that acknowledges a keystroke and one that merely updates.
anticipationnumber0.12How far a cell compresses before it springs back. A brief gather in the opposite direction is what makes the pop read as the cell reacting rather than as the cell being resized — at zero it is a plain overshoot and noticeably flatter.
pasteStaggernumber45Delay between cells as a pasted or autofilled code distributes. Zero fills every cell on one frame, which is honest and reads as a glitch; the stagger turns an instantaneous state change into something the eye can follow.
shakeAmplitudenumber10Lateral travel of the rejection. It moves the whole row rather than the cells individually — a row that shakes has been judged, a set of cells that each shake looks broken.
autoSubmitbooleantrueJudge the code the moment the last cell fills instead of waiting for a button. Correct when a wrong answer costs nothing; wrong the instant it does.
submitDelaynumber220Pause between the last character landing and the verdict. It exists so the final cell finishes its own pop before the row is judged — validating on the same frame steals the confirmation of the keystroke that caused it, and the user never learns their last character registered.
accentColorstring (hex)"#a855f7"The focused cell's ring, the filled border, the caret, and the success tick.
errorColorstring (hex)"#f87171"The rejection. It takes the cell borders rather than flooding the fills, so the code you typed stays readable while you are being told it is wrong.

What It Demonstrates

Spring
Motion driven by physics (tension, mass, damping) rather than a set duration.
Stiffness / Tension
How strongly the spring pulls toward its target. Higher feels snappier.
Anticipation
A small wind-up in the opposite direction before a move, hinting at what's about to happen.
Scale
Make an element bigger or smaller.
Pop in
Element appears with a slight overshoot, like it bounces into place.
Stagger
Animate several items one after another with a small delay between each, creating a cascade.
Shake / Wiggle
A quick side-to-side jitter signaling an error or rejected input.
Morph
One shape smoothly turns into another shape, e.g. Dynamic Island.

Related Components

Appears In