Morphing Checkout Flow
moumenlab/morphing-checkoutFreeComponent
A three-step card payment with a height:auto illusion, caret-safe masking, a 3D flip, and a FLIP pay button.
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/morphing-checkout.jsonSource
1"use client";23import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";4import { MotionConfig, motion, useReducedMotion } from "motion/react";56// Morphing checkout flow - a three-step card payment (card → billing → confirm)7// where ONE container appears to animate `height: auto` between steps.8//9// The body's height is always an explicit px measured off the active panel10// (useLayoutEffect + ResizeObserver); motion eases it. Everything else is11// transform/opacity: the outgoing step exits softly while the incoming one's12// fields cascade in, direction-aware. The other hard constraints:13//14// · CARET-PRESERVING MASK. The number re-formats every keystroke (4-4-4-4, or15// 4-6-5 for an Amex); the caret is put back by counting the DIGITS before it16// (the only characters the user owns), so it never jumps.17// · LUHN. The checksum validated live - a full number that fails shakes; one18// that passes gets a quiet green check.19// · 3D FLIP. Two stacked faces under perspective/preserve-3d/backface-hidden;20// focusing the CVC rotates the card 180° (Amex prints its code on the FRONT,21// so an Amex never flips - the detection is real).22// · PAY MORPH. Paying FLIPs the button's width to a circle (measured px → rem,23// imperative) while label → spinner → drawn check cross-fade. A decline24// lands a red drawn ✕ with one shake, then eases back for another try.25//26// Pass `onPay` to run the real charge. Animation via motion/react; honours27// prefers-reduced-motion. Plain Tailwind only - no theme tokens or custom28// classes required.2930const EASE = [0.22, 1, 0.36, 1] as const;31const EASE_ICON = [0.2, 0, 0, 1] as const;32const STEP_NAMES = ["card", "billing", "confirm", "paid"];3334const BRANDS: Record<string, { label: string; length: number; cvc: number; cvcName: string; groups: number[] }> = {35 visa: { label: "Visa", length: 16, cvc: 3, cvcName: "CVC", groups: [4, 4, 4, 4] },36 mastercard: { label: "Mastercard", length: 16, cvc: 3, cvcName: "CVC", groups: [4, 4, 4, 4] },37 amex: { label: "Amex", length: 15, cvc: 4, cvcName: "CID", groups: [4, 6, 5] },38 unknown: { label: "Card", length: 16, cvc: 3, cvcName: "CVC", groups: [4, 4, 4, 4] },39};4041export interface CheckoutState {42 step: string;43 brand: string;44 number: string;45 side: string;46 status: string;47 height: number | null;48}4950// … truncated
What it pulls in
npm packages
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 | |
| OTP Segmented Inputotp-segmented-input | moumenlab | Components | Free | 1 dep | |
| Schedule Builderschedule-builder | moumenlab | Components | Free | 1 dep |
