Display

Credit Card

A flippable bank card display — masked number, holder and expiry on the front, magstripe and a hidden CVC placeholder on the back.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

export interface CreditCardProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Cardholder name, rendered as given */
  holder: string
  /** The full number; only the last 4 are shown and the leading digits render as •••• groups — masking is a display concern, so callers need not truncate first */
  number: string
  /** Expiry, e.g. "09/28" */
  expiry: string
  /** Text wordmark in the top-right, e.g. "VISA". Plain text — no brand logos */
  brand?: string

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/credit-card.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "CreditCard" component (no runtime
dependencies beyond React and the shared cn() utility).

Contract
- Export a forwardRef<HTMLDivElement, CreditCardProps> extending
  React.HTMLAttributes<HTMLDivElement>.
- Props: holder: string, number: string (full PAN in — the component does
  the redaction, callers don't need to pre-truncate), expiry: string (e.g.
  "09/28"), brand?: string (a plain-text wordmark like "VISA", not a logo
  image), flippable?: boolean (default true), className.
- No cvc prop at all — the component never receives or displays a real CVC,
  only a static "•••" placeholder on the back. This is a display-only
  component, not a PCI-scoped payment form.

Behavior
- Mask the number: strip non-digits, keep the last 4, always render
  "•••• •••• •••• 1234". Never render digits 1–12.
- Click, Enter or Space (when flippable) toggles a `flipped` boolean that
  rotates an inner wrapper 180° around the Y axis to reveal the back;
  clicking again flips it back.
- The root is a perspective container; the rotating inner element carries
  transform-3d (transform-style: preserve-3d) plus a ~500ms transition.
  Each face is absolute inset-0 backface-hidden, with the back face
  pre-rotated 180° so it lands right-side-up after the flip.
- Under prefers-reduced-motion: reduce, drop the transition duration to 0
  (instant face swap, no animated rotation) and drop the perspective too —
  reduced-motion users get a plain instant switch, never a spinning card.
- When flippable is false: no role/tabIndex/aria-pressed/click handler at
  all — a static, non-interactive front-only display (the back face still
  exists in the DOM but is never reachable).
- Whichever face is rotated away is aria-hidden, so screen readers only
  ever see the face that's visually showing.

Rendering & styling
- Card face: bg-gradient-to-br from-primary via-primary/80 to-primary/60
  text-primary-foreground — no hardcoded colors, so the card re-themes with
  the host palette and dark mode for free.
- Chip: a small 2×2 grid of rounded squares, bg-primary-foreground/80
  container with bg-primary/50 pads — CSS only, no image.
- Magstripe: a full-width bg-foreground/80 bar near the top of the back
  face. Signature strip: a bg-primary-foreground/90 bar; the CVC
  placeholder sits in a small bg-primary-foreground/90 chip with
  text-foreground.
- Number is font-mono tracking-widest. Merge className via cn().
- Root sizing: aspect-[8/5] max-w-sm, so it drops into a settings row or a
  grid cell at a predictable shape.

Customization levers
- Color scheme: swap the gradient for its inverse — bg-foreground
  text-background — for a dark/mono card instead of the primary-tinted one.
- Size: max-w-sm is the only sizing knob; override via className, the
  aspect ratio keeps the layout correct at any width.
- Disable flip entirely: flippable={false} for a purely static card when
  you never need the back (e.g. a compact list row).
- Brand: the brand slot is plain text on purpose (no bundled network
  logos); swap it for an <img> at the call site if you want real card
  network marks.
- Never wrap this in a real payment form: for actual PAN/CVC capture use
  Stripe Elements (or your processor's hosted fields) — this component,
  and any hand-rolled input, is display-only and out of PCI scope by
  design.

Concepts

  • Click/keyboard fliprole="button" + tabIndex + manual onKeyDown for Enter/Space; it's a plain div, not a real <button>, so key handling has to be wired by hand.
  • 3D flip via CSS transformsperspective on the parent, transform-3d + rotate-y-180 + transition on the flipper, backface-hidden on each face so only the front-facing side ever paints.
  • PAN masking — the full card number goes in, only the last 4 digits ever render; masking happens inside the component regardless of what the caller passes in.
  • Reduced-motion honesty — under prefers-reduced-motion, the flip becomes an instant face swap with zero transition and no perspective depth, never a spinning card.
  • Display-only, not a payment form — there is no cvc prop; this renders card data, it never captures it. Real PAN/CVC entry belongs to Stripe Elements or an equivalent hosted-fields form.

On This Page