bottega

cards

Credit Wallet Card

A CSS-only wallet widget showing three stacked payment cards (primary/accent/background surfaces) held in a leather-look SVG pocket. Hover the wallet to lift it and fan the cards out with spring-eased transforms; hover an individual card to pop it to the front and reveal the full card number. The hidden balance reveals with a fade+slide transition and the eye icon toggles open. Zero JS — all interaction is pure CSS :hover. Entry animation slides cards in from above on mount. Reduced-motion: entry animation and fan transforms are disabled; balance/number reveal on hover still works instantly.

Ported from byllzz (MIT)


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/credit-wallet-card

Usage

Usagetsx
<CreditWalletCard />

Props

PropTypeDefaultDescription
holderNamestringALEX SMITHName shown on the primary (top) card.
businessNamestringSTUDIO LLCLabel shown on the accent (middle) card.
emailstringhello@work.comEmail shown on the light (front) card.
balancestring$12,450.00Balance revealed when the wallet is hovered.
classNamestringExtra class name merged onto the root element.

Source

credit-wallet-card.tsxtsx
import type React from "react";
import styles from "./credit-wallet-card.module.css";

export type CreditWalletCardProps = {
  /** Name shown on the primary (top) card. */
  holderName?: string;
  /** Label shown on the accent (middle) card. */
  businessName?: string;
  /** Email shown on the light (front) card. */
  email?: string;
  /** Balance revealed when the wallet is hovered. */
  balance?: string;
  /** Extra class name merged onto the root element. */
  className?: string;
};

function EyeSlashIcon() {
  return (
    <svg
      className={`${styles.eyeIcon} ${styles.eyeSlash}`}
      aria-hidden="true"
      width="20"
      height="20"
      viewBox="0 0 24 24"
      fill="none"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
      <circle cx="12" cy="12" r="3" />
      <line x1="3" y1="3" x2="21" y2="21" />
    </svg>
  );
}

function EyeOpenIcon() {
  return (
    <svg
      className={`${styles.eyeIcon} ${styles.eyeOpen}`}
      aria-hidden="true"
      width="20"
      height="20"
      viewBox="0 0 24 24"
      fill="none"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
      <circle cx="12" cy="12" r="3" />
    </svg>
  );
}

/**
 * CreditWalletCard — A CSS-only wallet widget with three stacked payment cards
 * in a leather-style SVG pocket. Hover the wallet to fan out the cards and
 * reveal the hidden balance; hover an individual card to pop it up and show the
 * full card number. No JS or state — pure CSS `:hover` transitions throughout.
 * Fully server-renderable.
 */
export function CreditWalletCard({
  holderName = "ALEX SMITH",
  businessName = "STUDIO LLC",
  email = "hello@work.com",
  balance = "$12,450.00",
  className,
}: CreditWalletCardProps) {
  return (
    <div
      className={[styles.wallet, className].filter(Boolean).join(" ")}
      role="img"
      aria-label="Credit wallet"
    >
      {/* Leather back of the wallet */}
      <div className={styles.walletBack} aria-hidden="true" />

      {/* Card 1 — primary surface */}
      <div className={`${styles.card} ${styles.cardPrimary}`}>
        <div className={styles.cardInner}>
          <div className={styles.cardTop}>
            <span>ARCO</span>
            <div className={styles.chip} aria-hidden="true" />
          </div>
          <div className={styles.cardBottom}>
            <div className={styles.cardInfo}>
              <span className={styles.label}>Holder</span>
              <span className={styles.value}>{holderName}</span>
            </div>
            <div className={styles.cardNumberWrapper}>
              <span className={styles.hiddenStars}>**** 4242</span>
              <span className={styles.cardNumber}>5524 9910 4242</span>
            </div>
          </div>
        </div>
      </div>

      {/* Card 2 — accent surface */}
      <div className={`${styles.card} ${styles.cardAccent}`}>
        <div className={styles.cardInner}>
          <div className={styles.cardTop}>
            <span>ATELIER</span>
            <div className={styles.chip} aria-hidden="true" />
          </div>
          <div className={styles.cardBottom}>
            <div className={styles.cardInfo}>
              <span className={styles.label}>Business</span>
              <span className={styles.value}>{businessName}</span>
            </div>
            <div className={styles.cardNumberWrapper}>
              <span className={styles.hiddenStars}>**** 8810</span>
              <span className={styles.cardNumber}>9012 4432 8810</span>
            </div>
          </div>
        </div>
      </div>

      {/* Card 3 — light (background) surface */}
      <div className={`${styles.card} ${styles.cardLight}`}>
        <div className={styles.cardInner}>
          <div className={styles.cardTop}>
            <span>CARTE</span>
            <div className={styles.chip} aria-hidden="true" />
          </div>
          <div className={styles.cardBottom}>
            <div className={styles.cardInfo}>
              <span className={styles.label}>Email</span>
              <span className={styles.value}>{email}</span>
            </div>
            <div className={styles.cardNumberWrapper}>
              <span className={styles.hiddenStars}>**** 0094</span>
              <span className={styles.cardNumber}>3312 0045 0094</span>
            </div>
          </div>
        </div>
      </div>

      {/* SVG-shaped pocket that holds the cards */}
      <div className={styles.pocket} aria-hidden="true">
        <svg
          className={styles.pocketSvg}
          viewBox="0 0 280 160"
          fill="none"
          aria-hidden="true"
        >
          {/* Pocket body fill */}
          <path
            className={styles.pocketPath}
            d="M 0 20 C 0 10, 5 10, 10 10 C 20 10, 25 25, 40 25 L 240 25 C 255 25, 260 10, 270 10 C 275 10, 280 10, 280 20 L 280 120 C 280 155, 260 160, 240 160 L 40 160 C 20 160, 0 155, 0 120 Z"
          />
          {/* Pocket decorative dashed border */}
          <path
            className={styles.pocketBorder}
            d="M 8 22 C 8 16, 12 16, 15 16 C 23 16, 27 29, 40 29 L 240 29 C 253 29, 257 16, 265 16 C 268 16, 272 16, 272 22 L 272 120 C 272 150, 255 152, 240 152 L 40 152 C 25 152, 8 152, 8 120 Z"
          />
        </svg>

        <div className={styles.pocketContent}>
          {/* Balance row: stars hidden until hover reveals the real amount */}
          <div className={styles.balanceWrapper}>
            <div className={styles.balanceStars}>******</div>
            <div className={styles.balanceReal}>{balance}</div>
          </div>
          <div className={styles.balanceTotalLabel}>Total Balance</div>
          <div className={styles.eyeIconWrapper}>
            <EyeSlashIcon />
            <EyeOpenIcon />
          </div>
        </div>
      </div>
    </div>
  );
}

Dependencies

  • @bottega/tokens