UIHigh-End Animated Cardsno dependencies

CreditCard

A dark payment card with chip, number, holder and expiry, whose warm glow follows the pointer with smoothing and an optional 3D tilt, settling back to a soft rest glow when the pointer leaves.

Preview

Customize

10 props
380

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add credit-card

Writes components/ui/credit-card.tsx and components/styles/credit-card.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { CreditCard } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <CreditCard />6  );7}

Props

PropTypeDefault
brand

Issuer text in the top-left corner.

string'MOTIONFOUNDRY'
number

Card number, spaces kept as typed.

string'4242 4242 4242 4242'
holder

Cardholder name.

string'ALEX MORGAN'
expiry

Expiry date.

string'12/28'
glowColor

Pointer-following glow color.

string'#ff8a1f'
chipColor

EMV chip color.

string'#9a5617'
cardColor

Card surface color.

string'#1a1817'
tilt

Tilt the card in 3D toward the pointer.

booleantrue
maskNumber

Show only the last four digits.

booleanfalse
width

Width in pixels; height follows the ISO card ratio.

number380
className

Merged onto the root.

string''
style

Merged onto the root.

React.CSSPropertiesundefined

Accessibility

Exposed as a single role="img" with a descriptive label (brand, last four digits, holder, expiry) so the decorative layout isn't read piece by piece; the full number is never announced. Pointer effects are visual only, and under prefers-reduced-motion the tilt is disabled and the glow jumps without easing.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/credit-card.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/credit-card.module.css";5 6export interface CreditCardProps {7  /** Issuer / brand text in the top-left corner */8  brand?: string;9  /** Card number (spaces are kept as typed) */10  number?: string;11  /** Cardholder name */12  holder?: string;13  /** Expiry date */14  expiry?: string;15  /** Glow color that follows the pointer */16  glowColor?: string;17  /** EMV chip color */18  chipColor?: string;19  /** Card surface color */20  cardColor?: string;21  /** Tilt the card in 3D toward the pointer */22  tilt?: boolean;23  /** Mask all but the last four digits */24  maskNumber?: boolean;25  /** Card width in pixels (height follows the ISO card ratio) */26  width?: number;27  /** Optional className merged onto the root */28  className?: string;29  /** Optional inline styles merged onto the root */30  style?: React.CSSProperties;31}32 33const REST = { x: 0.5, y: 0.85 };34 35function masked(value: string): string {36  const digits = value.replace(/\D/g, "");37  const last = digits.slice(-4);38  return `•••• •••• •••• ${last}`;39}40 41export function CreditCard({42  brand = "MOTIONFOUNDRY",43  number = "4242 4242 4242 4242",44  holder = "ALEX MORGAN",45  expiry = "12/28",46  glowColor = "#ff8a1f",47  chipColor = "#9a5617",48  cardColor = "#1a1817",49  tilt = true,50  maskNumber = false,51  width = 380,52  className = "",53  style,54}: CreditCardProps) {55  const cardRef = React.useRef<HTMLDivElement>(null);56  const target = React.useRef({ ...REST, hover: 0 });57  const current = React.useRef({ ...REST, hover: 0 });58  const frame = React.useRef(0);59  const reduceMotion = React.useRef(false);60 61  React.useEffect(() => {62    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");63    reduceMotion.current = mq.matches;64    const onChange = () => (reduceMotion.current = mq.matches);65    mq.addEventListener("change", onChange);66    return () => {67      mq.removeEventListener("change", onChange);68      cancelAnimationFrame(frame.current);69    };70  }, []);71 72  // Ease the glow (and tilt) toward the pointer; stop the loop once it settles.73  const tick = React.useCallback(() => {74    const el = cardRef.current;75    if (!el) return;76    const t = target.current;77    const c = current.current;78    const k = reduceMotion.current ? 1 : 0.14;79    c.x += (t.x - c.x) * k;80    c.y += (t.y - c.y) * k;81    c.hover += (t.hover - c.hover) * k;82    el.style.setProperty("--mf-cc-x", `${(c.x * 100).toFixed(2)}%`);83    el.style.setProperty("--mf-cc-y", `${(c.y * 100).toFixed(2)}%`);84    el.style.setProperty("--mf-cc-hover", c.hover.toFixed(3));85    const tiltOn = tilt && !reduceMotion.current;86    el.style.setProperty("--mf-cc-rx", tiltOn ? `${((0.5 - c.y) * 14 * c.hover).toFixed(2)}deg` : "0deg");87    el.style.setProperty("--mf-cc-ry", tiltOn ? `${((c.x - 0.5) * 18 * c.hover).toFixed(2)}deg` : "0deg");88    const settled = Math.abs(t.x - c.x) + Math.abs(t.y - c.y) + Math.abs(t.hover - c.hover) < 0.001;89    frame.current = settled ? 0 : requestAnimationFrame(tick);90  }, [tilt]);91 92  const kick = () => {93    if (!frame.current) frame.current = requestAnimationFrame(tick);94  };95 96  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {97    const rect = e.currentTarget.getBoundingClientRect();98    target.current = { x: (e.clientX - rect.left) / rect.width, y: (e.clientY - rect.top) / rect.height, hover: 1 };99    kick();100  };101 102  const onPointerLeave = () => {103    target.current = { ...REST, hover: 0 };104    kick();105  };106 107  const shown = maskNumber ? masked(number) : number;108  const last4 = number.replace(/\D/g, "").slice(-4);109 110  return (111    <div112      className={`${styles.root} ${className}`.trim()}113      style={114        {115          "--mf-cc-width": `${width}px`,116          "--mf-cc-glow": glowColor,117          "--mf-cc-chip": chipColor,118          "--mf-cc-card": cardColor,119          ...style,120        } as React.CSSProperties121      }122    >123      <div124        ref={cardRef}125        role="img"126        aria-label={`${brand} card ending in ${last4}, ${holder}, expires ${expiry}`}127        className={styles.card}128        onPointerMove={onPointerMove}129        onPointerLeave={onPointerLeave}130      >131        <span className={styles.glow} aria-hidden="true" />132        <span className={styles.sheen} aria-hidden="true" />133        <div className={styles.top} aria-hidden="true">134          <span className={styles.brand}>{brand}</span>135          <span className={styles.chip}>136            <span />137            <span />138          </span>139        </div>140        <div className={styles.number} aria-hidden="true">141          {shown}142        </div>143        <div className={styles.bottom} aria-hidden="true">144          <span>{holder}</span>145          <span>{expiry}</span>146        </div>147      </div>148    </div>149  );150}