JuandaGarcia/crd-ui

πŸ’³ A credit & debit card component. Dependency-free, themeable, localizable. One package for React, Vue, Svelte and vanilla JS.

TypeScript

22

99 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

react-credit-cards won't install on React 18 or 19 (ERESOLVE) β€” I built a maintained replacement with the same API (r/reactjs)

If you've built a payment form in React you've probably used `react-credit-cards`. It's still at \~25k downloads a week, its last release was June 2020, and it peers on `react@^15 || ^16` β€” so on any current React, npm refuses to install it: npm error ERESOLVE unable to resolve dependency tree npm…

1

Sep 28, 2026

README

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).

Starter: a checkout form with a live card preview β€” open it in StackBlitz, or copy the React or Next.js version.

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

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.

card
checkout
component-library
credit-card
debit-card
payment
payment-form
react
stripe
svelte
typescript
ui-component
vanilla-js
vue

JuandaGarcia/crd-ui

πŸ’³ A credit & debit card component. Dependency-free, themeable, localizable. One package for React, Vue, Svelte and vanilla JS.

TypeScript

22

99 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

react-credit-cards won't install on React 18 or 19 (ERESOLVE) β€” I built a maintained replacement with the same API (r/reactjs)

If you've built a payment form in React you've probably used `react-credit-cards`. It's still at \~25k downloads a week, its last release was June 2020, and it peers on `react@^15 || ^16` β€” so on any current React, npm refuses to install it: npm error ERESOLVE unable to resolve dependency tree npm…

1

Sep 28, 2026

README

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).

Starter: a checkout form with a live card preview β€” open it in StackBlitz, or copy the React or Next.js version.

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

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.

card
checkout
component-library
credit-card
debit-card
payment
payment-form
react
stripe
svelte
typescript
ui-component
vanilla-js
vue

Languages

TypeScript

47.8%

Astro

31.5%

CSS

18.8%