crd-ui.js: Animated Credit Card Preview UI for Payment Forms

Category: Form , Javascript | August 24, 2026
AuthorJuandaGarcia
Last UpdateAugust 24, 2026
LicenseMIT
Views0 views
crd-ui.js: Animated Credit Card Preview UI for Payment Forms

crd-ui is a framework-agnostic credit and debit card component that mirrors payment-form data in a live visual card preview.

It formats and masks card details, detects the card brand while the number changes, highlights the active field, and flips to the back when the CVC field receives focus.

Features

  • Live formatting and masking for card numbers, names, expiry dates, and CVC values.
  • Automatic detection for Visa, Mastercard, Amex, Discover, Diners Club, JCB, UnionPay, Maestro, Elo, and Hipercard.
  • Animated 3D card flip when the CVC field receives focus.
  • Moving focus highlight for the card number, cardholder name, and expiry date.
  • Form and saved-card display layouts.
  • 6 built-in card finishes.
  • Optional pointer-tracked 3D tilt and glare.
  • CSS custom properties and stable styling slots.
  • Custom SVG brand marks and localized labels.
  • Reduced-motion handling for card animations.

How To Use It

Installation

Install the package through npm:

npm install crd-ui

Vanilla JavaScript projects import createCard from the package root. The standard card styles come from crd-ui/styles.css.

import { createCard } from 'crd-ui';
import 'crd-ui/styles.css';

Basic Usage

crd-ui renders the visual card. Your payment fields remain normal form controls. Send each field value and its focus state to the card instance as the visitor edits the form.

<div id="payment-card-preview"></div>
<label>
  Card number
  <input type="text" data-card-field="number" autocomplete="cc-number">
</label>
<label>
  Cardholder name
  <input type="text" data-card-field="name" autocomplete="cc-name">
</label>
<label>
  Expiry
  <input type="text" data-card-field="expiry" autocomplete="cc-exp">
</label>
<label>
  CVC
  <input type="text" data-card-field="cvc" autocomplete="cc-csc">
</label>
import { createCard } from 'crd-ui';
import 'crd-ui/styles.css';
const card = createCard(
  document.querySelector('#payment-card-preview')
);
document.querySelectorAll('[data-card-field]').forEach((input) => {
  const field = input.dataset.cardField;
  input.addEventListener('input', () => {
    card.update({
      [field]: input.value
    });
  });
  input.addEventListener('focus', () => {
    card.update({
      focused: field
    });
  });
  input.addEventListener('blur', () => {
    card.update({
      focused: null
    });
  });
});

Card Finishes and Hover Tilt

The variant option selects one of six finishes: sunset, ember, holo, porcelain, graphite, or gradient. sunset is the default. Set tilt to true when the card should react to pointer movement on hover-capable devices.

const card = createCard(
  document.querySelector('#payment-card-preview'),
  {
    variant: 'holo',
    tilt: true
  }
);
// Change the finish later.
card.update({
  variant: 'graphite'
});

Display Saved Cards

The display layout presents an existing payment card in dashboards, wallets, and saved-payment screens. Start with the brand and last4. Supply sensitive values only when your application has securely retrieved them.

copyable makes revealed number, expiry, and CVC values clickable. The onCopy callback runs after a copy action.

const savedCard = createCard(
  document.querySelector('#saved-card'),
  {
    layout: 'display',
    brand: 'mastercard',
    last4: '4826',
    variant: 'graphite',
    copyable: true,
    onCopy(field, value) {
      console.log(`${field} copied`);
    }
  }
);
document.querySelector('#reveal-card').addEventListener('click', () => {
  savedCard.update({
    number: '5555 5555 5555 4826',
    expiry: '09/29',
    cvc: '318'
  });
});

Configuration Options

  • number (string): Card number displayed with brand-aware formatting and masking.
  • name (string): Cardholder name.
  • expiry (string): Expiry value normalized to MM/YY.
  • cvc (string): Security code displayed on the card.
  • focused ('number' | 'name' | 'expiry' | 'cvc' | null): Sets the active card section. cvc flips the card in form layout.
  • variant ('sunset' | 'ember' | 'holo' | 'porcelain' | 'graphite' | 'gradient'): Selects the card finish. The default is sunset.
  • tilt (boolean): Activates pointer-tracked 3D tilt and glare. The default is false.
  • brand (Brand | null): Overrides automatic brand detection. Omit it to detect the brand from number.
  • last4 (string): Displays masked card digits when the full number is unavailable.
  • layout ('form' | 'display'): Selects the payment-form preview or saved-card presentation. The default is form.
  • copyable (boolean): Activates click-to-copy for revealed values in display layout. The default is false.
  • classNames (Partial<Record<CardSlot, string>>): Adds custom classes to named card sections.
  • placeholders ({ name?: string }): Changes the empty cardholder-name placeholder.
  • locale ({ validThru?, exp?, cvc?, copy?, copied? }): Changes card labels and copy feedback text.
  • logos (Partial<Record<Brand, string>>): Supplies custom inline SVG marks for card brands.
  • onCopy ((field, value) => void): Runs after a revealed display-layout field is copied.

Instance API

createCard() returns an instance that can update the card, expose its detected brand and root element, or remove the rendered component.

// Merge new state into the current card.
card.update({
  name: 'Jordan Lee',
  expiry: '11/30'
});
// Read the currently detected brand.
console.log(card.brand);
// Access the rendered .crd root element.
console.log(card.element);
// Remove the card from the DOM.
card.destroy();

Styling and Customization

The core card styles read public --crd-* custom properties. Set them on .crd or an ancestor.

The main layout and appearance controls include:

  • --crd-width: Card width. Default 290px.
  • --crd-radius: Corner radius. Default 14px.
  • --crd-font: Card font family.
  • --crd-color: Main text and foreground color.
  • --crd-bg: Full card background value.
  • --crd-shadow: Card shadow.
  • --crd-flip-duration: Duration of the flip animation. Default 0.75s.

A full CSS background works for --crd-bg, including gradients and local images.

.checkout-preview {
  --crd-width: 340px;
  --crd-radius: 20px;
  --crd-font: "IBM Plex Mono", monospace;
  --crd-color: #fff;
  --crd-bg: linear-gradient(135deg, #172033, #3c506f);
  --crd-shadow: 0 14px 35px rgb(0 0 0 / 30%);
  --crd-flip-duration: 0.6s;
}

The classNames option reaches individual card sections when a project needs utility classes or more specific styling. Available slots are root, inner, front, back, chip, logo, number, footer, name, expiry, expiryLabel, expiryValue, meta, metaExpiry, metaCvc, and cvc.

const card = createCard(
  document.querySelector('#payment-card-preview'),
  {
    classNames: {
      root: 'payment-card',
      number: 'payment-card-number',
      name: 'payment-card-name',
      cvc: 'payment-card-cvc'
    }
  }
);

Tailwind Styling

CSS custom properties work with Tailwind arbitrary-property utilities. Tailwind utilities that need to override the component’s own style declarations require the cascade-layer build.

Define the layer order in your application CSS:

@layer crd-ui, theme, base, components, utilities;
@import "tailwindcss";

Load the layered stylesheet in the application:

import 'crd-ui/styles.layer.css';

A React card can then receive CSS variables through className:

<Card
  className="
    [--crd-radius:1.25rem]
    [--crd-color:white]
    [--crd-bg:var(--color-indigo-600)]
  "
/>

Custom Brand Logos

crd-ui ships generic brand graphics. Applications licensed to display official card-network artwork can supply inline SVG markup through logos.

import { createCard, LOGOS } from 'crd-ui';
const card = createCard(
  document.querySelector('#payment-card-preview'),
  {
    logos: {
      ...LOGOS,
      visa: customVisaSvg
    }
  }
);

Localization

Card labels can be changed through placeholders and locale. The display layout also exposes labels for expiry, CVC, copy hints, and copied-state feedback.

createCard(
  document.querySelector('#payment-card-preview'),
  {
    placeholders: {
      name: 'NOMBRE COMPLETO'
    },
    locale: {
      validThru: 'válida hasta',
      exp: 'Vence',
      cvc: 'Código',
      copy: 'Copiar',
      copied: 'Copiado'
    }
  }
);

React, Vue, and Svelte Adapters

The package includes framework adapters under subpath imports. React requires version 18 or newer, Vue requires version 3 or newer, and Svelte requires version 5 or newer.

React imports the Card component from crd-ui/react:

import { Card } from 'crd-ui/react';
import 'crd-ui/styles.css';
<Card
  number={number}
  name={name}
  expiry={expiry}
  cvc={cvc}
  focused={focused}
/>

Vue 3 imports Card from crd-ui/vue. Brand changes are available through the brand-change event:

<script setup>
import { Card } from 'crd-ui/vue';
import 'crd-ui/styles.css';
</script>
<template>
  <Card
-number="number"
-focused="focused"
    @brand-change="handleBrandChange"
  />
</template>

Svelte 5 imports the component from crd-ui/svelte:

<script>
  import Card from 'crd-ui/svelte';
  import 'crd-ui/styles.css';
</script>
<Card {number} {focused} />

Stripe Elements Integration

A payment provider can keep the card number inside its secured field while crd-ui receives only presentation state. brand supplies the detected card network and focused synchronizes the active field.

The package also exports brandFromStripe() to normalize Stripe brand identifiers.

import {
  Card,
  brandFromStripe
} from 'crd-ui/react';
<Card
  number=""
  brand={brand}
  focused={focused}
/>
// Example inside a Stripe CardNumberElement handler.
const handleCardChange = (event) => {
  setBrand(brandFromStripe(event.brand));
};

Alternatives

You Might Be Interested In:


Leave a Reply