crd-ui

Credit/debit card preview for payment forms. Zero-dependency vanilla core with React, Vue and Svelte subpaths. A maintained react-credit-cards alternative.
15,227 35
Install
npm install crd-ui
Latest Version:0.13.2
Vue:3
License:MIT
Last Updated:Oct 6, 2026
Links: GitHub  ·  npm
Maintainer: juandagarcia

crd-ui — a credit & debit card component. Dependency-free. Themeable. Localizable.

crd-ui

npm downloads bundle size license

Framework-agnostic credit/debit card visualization for your payment forms. One package: a zero-dependency vanilla core, with framework adapters as subpaths.

npm i crd-ui
  • crd-ui — the vanilla core: brand detection, formatting and the card renderer.
  • crd-ui/react — React component (<Card />).
  • crd-ui/vue — Vue 3 component (<Card />).
  • crd-ui/svelte — Svelte 5 component (<Card />).
  • crd-ui/styles.css — the stylesheet (crd-ui/styles.layer.css for a cascade-layered build).

Features

  • 💳 Realistic card preview: formatted/masked number, name, expiry, CVC.
  • 🔄 Choreographed 3D flip when the CVC is focused: the card lifts off with a wrist-flick tilt, spring-settles past 180°, and a light sheen sweeps the face (prefers-reduced-motion aware).
  • 🎯 Magic focus ring: one highlight travels between sections, sliding and morphing to fit each one with a spring settle.
  • 🪩 Optional 3D hover tilt (tilt): the card follows the pointer with a cursor-tracked glare (hover-only — flattened on touch devices and under prefers-reduced-motion).
  • 🪪 Two layouts: 'form' (payment-form preview, default) and 'display' for presenting a card the user owns — dashboards, saved cards, with click-to-reveal.
  • 🏷 Live brand detection while typing: Visa, Mastercard, Amex, Discover, Diners Club, JCB, UnionPay, Maestro, Elo, Hipercard.
  • ✨ Six built-in finishes via variant — the default sunset tints its color bloom to the detected brand.
  • 🎨 Themeable via CSS custom properties; per-brand gradients out of the box.
  • 🌍 Localizable labels and placeholders.
  • 💜 Plays well with Stripe: the brand override + focused mirror Stripe Elements' metadata without ever touching the number — see examples/stripe.
  • 📦 Zero runtime dependencies (React/Vue/Svelte are optional peers, only for their subpaths).

React usage

import { useState } from 'react';
import { Card } from 'crd-ui/react';
import 'crd-ui/styles.css';

function PaymentForm() {
  const [number, setNumber] = useState('');
  const [focused, setFocused] = useState(null);

  return (
    <>
      <Card number={number} focused={focused} />
      <input
        value={number}
        onChange={(e) => setNumber(e.target.value)}
        onFocus={() => setFocused('number')}
        onBlur={() => setFocused(null)}
      />
      {/* name / expiry / cvc inputs alike */}
    </>
  );
}

Vanilla usage

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

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

numberInput.addEventListener('input', (e) => card.update({ number: e.target.value }));
cvcInput.addEventListener('focus', () => card.update({ focused: 'cvc' })); // flips
cvcInput.addEventListener('blur', () => card.update({ focused: null }));

card.brand;      // 'visa' | 'mastercard' | … | null
card.destroy();  // remove from the DOM

Vue usage

<script setup>
import { ref } from 'vue';
import { Card } from 'crd-ui/vue';
import 'crd-ui/styles.css';

const number = ref('');
const focused = ref(null);
</script>

<template>
  <Card :number="number" :focused="focused" @brand-change="(b) => console.log(b)" />
  <input
    v-model="number"
    @focus="focused = 'number'"
    @blur="focused = null"
  />
  <!-- name / expiry / cvc inputs alike -->
</template>

Svelte usage

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

  let number = $state('');
  let focused = $state(null);
</script>

<Card {number} {focused} />
<input
  bind:value={number}
  onfocus={() => (focused = 'number')}
  onblur={() => (focused = null)}
/>
<!-- name / expiry / cvc inputs alike -->

Variants

Pick the card's finish with the variant prop/option: 'sunset' (default) · 'ember' · 'holo' · 'porcelain' · 'graphite' · 'gradient'.

sunset is a light porcelain face with a color bloom that adapts to the detected brand; gradient is the classic dark per-brand gradient. The rest are brand-agnostic finishes — the brand still shows through its logo and the sunset bloom.

import { Card } from 'crd-ui/react';

<Card variant="holo" number={number} focused={focused} />;
import { createCard } from 'crd-ui';

const card = createCard(el, { variant: 'holo' });
card.update({ variant: 'graphite' });

Display layout

Set layout="display" to present a card the user already owns — dashboards, saved-card lists, wallet views. Expiry and CVC move to a meta row on the front, empty values stay masked, the empty name hides, and focusing the CVC no longer flips the card. Start with last4 and reveal by passing the real values (fetched securely on demand) — the component only presents; it never stores data.

import { useState } from 'react';
import { Card } from 'crd-ui/react';

function SavedCard() {
  const [revealed, setRevealed] = useState(false);
  const details = revealed
    ? { number: '5355 2400 0000 5460', expiry: '08/27', cvc: '123' }
    : {};

  return (
    <>
      <Card layout="display" brand="mastercard" last4="5460" variant="graphite" {...details} />
      <button onClick={() => setRevealed((r) => !r)}>
        {revealed ? 'Hide' : 'Reveal details'}
      </button>
    </>
  );
}
import { createCard } from 'crd-ui';

const card = createCard(el, { layout: 'display', brand: 'mastercard', last4: '5460' });
// later, when the user asks to reveal:
card.update({ number: '5355 2400 0000 5460', expiry: '08/27', cvc: '123' });

Add copyable to let the user click the revealed number, expiry and CVC to copy them (with a "Copied" bubble); an optional onCopy(field, value) fires after each copy for your own toast or analytics:

<Card layout="display" copyable brand="mastercard" last4="5460" {...details}
  onCopy={(field, value) => console.log('copied', field)} />

Theming

Override the custom properties on .crd or any ancestor — the defaults are var() fallbacks rather than declarations on the card, so an inherited value always reaches it:

.crd {
  --crd-width: 340px;
  --crd-radius: 18px;
  --crd-bg: linear-gradient(135deg, #111, #333);
  --crd-font: 'SF Mono', monospace;
}

Brand themes are plain CSS classes (.crd--brand-visa, …) you can redefine entirely.

With Tailwind

Every knob is a CSS custom property whose default is a var() fallback rather than a declaration on .crd, so Tailwind arbitrary-property utilities theme the card with zero config — on the card or on any ancestor, and they win over the brand and variant themes. className (Vue/Svelte: class) targets the card root:

{/* v4: use var(--color-*); v3: use theme(colors.*) */}
<Card className="[--crd-radius:1.25rem] [--crd-color:white]
  [--crd-bg:var(--color-indigo-600)]" />

--crd-bg takes a full background, so images work here too (underscores become spaces):

<Card variant="gradient"
  className="[--crd-bg:url('/textures/holo.png')_center/cover]" />

One case needs setup: utilities that override the card's own rules — text-2xl against the number's font size, say. Tailwind emits utilities inside @layer utilities, and unlayered CSS always beats layered CSS, so import the pre-layered build and order the layer first:

/* app.css */
@layer crd-ui, theme, base, components, utilities;
@import "tailwindcss";
import 'crd-ui/styles.layer.css'; // instead of crd-ui/styles.css

Styling sections (classNames)

To style the card's internal parts with utility classes, pass a classNames slot map. Your classes are merged with the built-ins (state modifiers stay intact):

<Card
  classNames={{
    root: 'shadow-2xl ring-1 ring-white/10',
    number: 'tracking-widest',
    name: 'uppercase',
    metaExpiry: 'tabular-nums opacity-80',
  }}
/>

Slots: root, inner, front, back, chip, logo, number, footer, name, expiry, expiryLabel, expiryValue, meta, metaExpiry, metaCvc, cvc.

Brand logos

The built-in marks are deliberately generic (plain wordmarks / abstract shapes) so the package ships no trademarked assets. If your product is licensed to display the official logos, pass your own SVG per brand:

createCard(el, { logos: { visa: '<svg …>…</svg>' } });

The built-ins are exported as LOGOS (a Record<Brand, string> of SVG markup) if you need to render the same marks elsewhere — a brand picker, a saved-card list — or want to extend rather than replace them:

import { LOGOS } from 'crd-ui';

createCard(el, { logos: { ...LOGOS, visa: myLicensedVisaSvg } });

Card backgrounds

The website ships a library of original card backgrounds — free to download and use, at the card's ratio so they fill --crd-bg without cropping:

https://crd-ui.juanda.co/#backgrounds

They're released under CC0 1.0 (public domain): use them anywhere, commercially included, no attribution required. The crd-ui library itself stays MIT.

Localization

createCard(el, {
  placeholders: { name: 'NOMBRE COMPLETO' },
  locale: { validThru: 'válida hasta' },
});

AI & agents

The documentation is available as plain markdown for LLMs and coding agents:

Coming from react-credit-cards?

react-credit-cards hasn't shipped a release since June 2020, and its fork react-credit-cards-2 exposes the same prop API — so one migration covers both.

On React 18 or 19, npm i react-credit-cards fails with ERESOLVE: it declares react@^15 || ^16 as a peer dependency. crd-ui supports React 18 and 19.

number, name, expiry, cvc and focused keep their names and values, so for most codebases the swap is the import and the stylesheet:

- import Cards from 'react-credit-cards';
- import 'react-credit-cards/es/styles-compiled.css';
+ import { Card } from 'crd-ui/react';
+ import 'crd-ui/styles.css';

Full migration guide → — every prop mapped, including the two that aren't drop-in (callback and acceptedCards).

Development

pnpm install
pnpm test    # vitest
pnpm build   # tsup: ESM + CJS + d.ts
pnpm dev     # playground

Roadmap

  • Prebuilt official-logo add-on pack (opt-in)
  • Bank/issuer custom themes gallery

Sponsor

crd-ui is free and MIT licensed. If it saved you time, you can support its upkeep:

Sponsor

License

MIT

Upgrading to 0.12.0

className (Vue/Svelte: class) now lands on the card root (.crd) instead of the container element the component mounts into — matching how other component libraries behave, and making CSS-variable theming through it actually work. If you were using it to position the card in a layout, move those classes to a wrapper of your own.

The stylesheet no longer declares the --crd-* knobs on .crd; it reads them with the defaults as var() fallbacks. Overrides keep working exactly as before, and now they also work from an ancestor and from utility classes.

Related Packages

@funkit/connect-js

Framework-agnostic build of the Funkit Connect SDK

1,242
duoyun-ui

A lightweight desktop UI component library, implemented using Gem

38,579 52
@any-design/anyui

A cute multi-framework UI component library

4,478 26
marqy

High-fidelity loops for modern interfaces.

837,868
verity-dl

A framework agnostic data layer built to handle the communication layer between...

8,583 7

Version History

Version Released License
0.13.2 MIT
0.13.1 MIT
0.13.0 MIT
0.12.0 MIT
0.11.0 MIT