
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 toMM/YY.cvc(string): Security code displayed on the card.focused('number' | 'name' | 'expiry' | 'cvc' | null): Sets the active card section.cvcflips the card in form layout.variant('sunset' | 'ember' | 'holo' | 'porcelain' | 'graphite' | 'gradient'): Selects the card finish. The default issunset.tilt(boolean): Activates pointer-tracked 3D tilt and glare. The default isfalse.brand(Brand | null): Overrides automatic brand detection. Omit it to detect the brand fromnumber.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 isform.copyable(boolean): Activates click-to-copy for revealed values in display layout. The default isfalse.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. Default290px.--crd-radius: Corner radius. Default14px.--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. Default0.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
- Enhance Your Online Store’s Checkout Process With CardJs
- Validating and Formatting Credit Card Inputs with Payment.js
- Tiny JS Library To Create Interactive Payment Form – DatPayment
- 10 Best Credit Card Form Plugins To Increase Conversion Rates







