Menu

Change language

GitHub repo

Payment Card

payment-cardui

Premium metallic credit-card visual with embossed type, holographic foil, and per-digit typing animation. Auto-detects brand (Visa, Mastercard, Amex, Discover), flips in 3D, and supports opt-in mouse-parallax tilt and shimmer. Pure presentational — pass typed values in via props.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/payment-card.json
Manifest:https://uipkge.dev/r/svelte/payment-card.jsonInstalls to:src/lib/components/ui/payment-card/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
numberstring—optional
namestring—optional
expirystring—optional
cvcstring—optional
brandCardBrand | 'auto'autooptional
flippedbooleanfalseoptional
variant'default' | 'compact'defaultoptional
tiltbooleanfalseoptional
shimmerbooleanfalseoptional
flipbooleantrueoptional
size'sm' | 'md' | 'lg'mdoptional
refHTMLDivElement | null—optional

Used by

Files installed (2)

  • src/lib/components/ui/payment-card/PaymentCard.svelte25.7 kB
    <script lang="ts" module>
      import type { HTMLAttributes } from 'svelte/elements'
    
      export type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
    
      export interface PaymentCardProps extends HTMLAttributes<HTMLDivElement> {
        number?: string
        name?: string
        expiry?: string
        cvc?: string
        brand?: CardBrand | 'auto'
        flipped?: boolean
        variant?: 'default' | 'compact'
        tilt?: boolean
        shimmer?: boolean
        flip?: boolean
        size?: 'sm' | 'md' | 'lg'
        ref?: HTMLDivElement | null
      }
    
      export function detectBrand(raw: string): CardBrand {
        const n = raw.replace(/\D/g, '')
        if (!n) return 'unknown'
        if (/^4/.test(n)) return 'visa'
        if (/^(5[1-5]|2[2-7])/.test(n)) return 'mastercard'
        if (/^3[47]/.test(n)) return 'amex'
        if (/^(6011|65|64[4-9])/.test(n)) return 'discover'
        return 'unknown'
      }
    
      export function formatMasked(raw: string, brand: CardBrand): string {
        const digits = raw.replace(/\D/g, '').slice(0, brand === 'amex' ? 15 : 16)
        const groups = brand === 'amex' ? [4, 6, 5] : [4, 4, 4, 4]
        const total = groups.reduce((a, b) => a + b, 0)
        const padded = (digits + '••••••••••••••••').slice(0, total)
        let i = 0
        return groups
          .map((g) => {
            const slice = padded.slice(i, i + g)
            i += g
            return slice
          })
          .join(' ')
      }
    
      export interface BrandVisual {
        cardClass: string
        accentGlow: string
        foilSheen: string
        guillocheColor: string
        textColor: string
        subtextColor: string
        embossStyle: string
        chipType: 'gold' | 'silver'
      }
    
      export const BRAND_VISUALS: Record<CardBrand, BrandVisual> = {
        visa: {
          cardClass:
            'bg-[radial-gradient(ellipse_at_top_right,#1e3a8a_0%,#0f2252_45%,#08122c_85%,#040a18_100%)] text-white shadow-[0_22px_45px_-12px_rgba(15,34,82,0.65),0_8px_16px_-8px_rgba(0,0,0,0.5)]',
          accentGlow: 'from-blue-400/20 via-indigo-500/10 to-transparent',
          foilSheen: 'from-white/20 via-blue-200/5 to-transparent',
          guillocheColor: 'rgba(96, 165, 250, 0.12)',
          textColor: 'text-white',
          subtextColor: 'text-blue-200/60',
          embossStyle: 'text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]',
          chipType: 'gold',
        },
        mastercard: {
          cardClass:
            'bg-[radial-gradient(ellipse_at_top_right,#2a0808_0%,#180608_35%,#0f0506_70%,#09090b_100%)] text-white shadow-[0_22px_45px_-12px_rgba(220,38,38,0.35),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
          accentGlow: 'from-red-500/25 via-amber-500/15 to-transparent',
          foilSheen: 'from-white/20 via-orange-300/10 to-transparent',
          guillocheColor: 'rgba(239, 68, 68, 0.1)',
          textColor: 'text-white',
          subtextColor: 'text-zinc-400',
          embossStyle: 'text-zinc-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
          chipType: 'gold',
        },
        amex: {
          cardClass:
            'bg-[radial-gradient(ellipse_at_top_right,#1e293b_0%,#0f172a_40%,#090d16_80%,#020617_100%)] text-slate-100 shadow-[0_22px_45px_-12px_rgba(15,23,42,0.7),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
          accentGlow: 'from-slate-300/20 via-teal-500/10 to-transparent',
          foilSheen: 'from-white/25 via-cyan-100/10 to-transparent',
          guillocheColor: 'rgba(148, 163, 184, 0.12)',
          textColor: 'text-slate-100',
          subtextColor: 'text-slate-400',
          embossStyle: 'text-slate-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
          chipType: 'silver',
        },
        discover: {
          cardClass:
            'bg-[radial-gradient(ellipse_at_top_right,#431407_0%,#270d04_45%,#1c0903_80%,#0c0401_100%)] text-white shadow-[0_22px_45px_-12px_rgba(194,65,12,0.45),0_8px_16px_-8px_rgba(0,0,0,0.6)]',
          accentGlow: 'from-orange-400/25 via-amber-600/15 to-transparent',
          foilSheen: 'from-white/20 via-amber-200/10 to-transparent',
          guillocheColor: 'rgba(251, 146, 60, 0.12)',
          textColor: 'text-white',
          subtextColor: 'text-amber-200/60',
          embossStyle: 'text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
          chipType: 'gold',
        },
        unknown: {
          cardClass:
            'bg-[radial-gradient(ellipse_at_top_right,#27272a_0%,#18181b_40%,#09090b_85%,#000000_100%)] text-white shadow-[0_22px_45px_-12px_rgba(0,0,0,0.7),0_8px_16px_-8px_rgba(0,0,0,0.5)]',
          accentGlow: 'from-zinc-400/15 via-zinc-600/10 to-transparent',
          foilSheen: 'from-white/18 via-zinc-300/5 to-transparent',
          guillocheColor: 'rgba(255, 255, 255, 0.06)',
          textColor: 'text-white',
          subtextColor: 'text-zinc-400',
          embossStyle: 'text-zinc-100 drop-shadow-[0_1px_2px_rgba(0,0,0,0.9)]',
          chipType: 'silver',
        },
      }
    </script>
    
    <script lang="ts">
      import { cn } from '$lib/utils'
    
      let {
        number = '',
        name = '',
        expiry = '',
        cvc = '',
        brand = 'auto',
        flipped = false,
        variant = 'default',
        tilt = false,
        shimmer = false,
        flip = true,
        size = 'md',
        class: className,
        ref = $bindable(null),
        ...restProps
      }: PaymentCardProps = $props()
    
      const detectedBrand = $derived<CardBrand>(brand !== 'auto' ? brand : detectBrand(number))
      const displayNumber = $derived(formatMasked(number, detectedBrand))
      const displayName = $derived((name.trim() || 'CARDHOLDER NAME').toUpperCase())
      const displayExpiry = $derived.by(() => {
        const raw = expiry.replace(/\D/g, '').slice(0, 4)
        if (!raw) return 'MM/YY'
        if (raw.length <= 2) return raw + '/YY'
        return `${raw.slice(0, 2)}/${raw.slice(2, 4)}`
      })
      const displayCvc = $derived.by(() => {
        const max = detectedBrand === 'amex' ? 4 : 3
        const digits = cvc.replace(/\D/g, '').slice(0, max)
        return digits || '•••'
      })
      const displayChars = $derived(displayNumber.split(''))
      const ariaLabel = $derived(
        `${detectedBrand === 'unknown' ? 'Card' : detectedBrand} ending ${displayNumber.replace(/[•\s]/g, '').slice(-4) || '••••'}, ${displayName}, expires ${displayExpiry}`,
      )
    
      // Character pop-in animation tracking (immutable Set swaps keep $state reactive).
      let prevDigits = $state('')
      let poppedIndexes = $state<ReadonlySet<number>>(new Set())
      let popClearTimer: ReturnType<typeof setTimeout> | null = null
    
      $effect(() => {
        const nextDigits = number.replace(/\D/g, '')
        const prev = prevDigits
        if (nextDigits.length > prev.length) {
          poppedIndexes = new Set([...poppedIndexes, nextDigits.length - 1])
          if (popClearTimer) clearTimeout(popClearTimer)
          popClearTimer = setTimeout(() => {
            poppedIndexes = new Set()
          }, 400)
        }
        prevDigits = nextDigits
      })
    
      function shouldPop(charIndex: number, ch: string): boolean {
        if (ch === '•' || ch === ' ') return false
        const strUpTo = displayNumber.slice(0, charIndex + 1)
        const digitCount = (strUpTo.match(/\d/g) || []).length
        return poppedIndexes.has(digitCount - 1)
      }
    
      // 3D parallax tilt.
      let tiltX = $state(0)
      let tiltY = $state(0)
      let isHovering = $state(false)
    
      function onMove(e: MouseEvent) {
        if (!tilt || !ref) return
        const rect = ref.getBoundingClientRect()
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        const px = (x / rect.width - 0.5) * 2
        const py = (y / rect.height - 0.5) * 2
        tiltX = -py * 10
        tiltY = px * 12
        isHovering = true
      }
    
      function onLeave() {
        if (!tilt) return
        tiltX = 0
        tiltY = 0
        isHovering = false
      }
    
      const innerTransform = $derived.by(() => {
        const flipDeg = flipped ? 180 : 0
        if (tilt && isHovering) {
          return `perspective(1200px) rotateX(${tiltX}deg) rotateY(${tiltY + flipDeg}deg) scale3d(1.02, 1.02, 1.02)`
        }
        return `perspective(1200px) rotateY(${flipDeg}deg)`
      })
    
      const sizeClass = $derived.by(() => {
        if (variant === 'compact') return 'w-[120px]'
        switch (size) {
          case 'sm':
            return 'w-[280px]'
          case 'lg':
            return 'w-[420px]'
          case 'md':
          default:
            return 'w-[360px]'
        }
      })
    
      const theme = $derived(BRAND_VISUALS[detectedBrand])
      const tierLabel = $derived(
        detectedBrand === 'amex' ? 'PLATINUM' : detectedBrand === 'mastercard' ? 'WORLD ELITE' : 'SIGNATURE',
      )
    </script>
    
    <div
      bind:this={ref}
      data-uipkge
      data-slot="payment-card"
      role="img"
      aria-label={ariaLabel}
      class={cn('group relative inline-block [perspective:1200px] select-none', sizeClass, className)}
      onmousemove={onMove}
      onmouseleave={onLeave}
      {...restProps}
    >
      <div
        class={cn(
          'relative aspect-[85.6/53.98] w-full transition-transform [transform-style:preserve-3d]',
          flip ? 'duration-[650ms] ease-[cubic-bezier(0.22,1,0.36,1)]' : 'duration-300',
        )}
        style:transform={innerTransform}
      >
        <!-- ================================= FRONT FACE ================================= -->
        <div
          class={cn(
            'pay-card-face absolute inset-0 overflow-hidden rounded-[16px] border border-white/15 [backface-visibility:hidden]',
            theme.cardClass,
          )}
        >
          <!-- Fine Guilloché & Security Geometric Wave Pattern -->
          <svg
            class="pointer-events-none absolute inset-0 size-full opacity-60 mix-blend-screen"
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 360 227"
            fill="none"
            aria-hidden="true"
          >
            <g stroke={theme.guillocheColor} stroke-width="0.75" stroke-opacity="0.7">
              <path d="M-40 40 C 60 180, 180 -40, 400 120" />
              <path d="M-40 55 C 60 195, 180 -25, 400 135" />
              <path d="M-40 70 C 60 210, 180 -10, 400 150" />
              <path d="M-40 85 C 60 225, 180 5, 400 165" />
              <path d="M-40 100 C 60 240, 180 20, 400 180" />
              <path d="M-40 115 C 60 255, 180 35, 400 195" />
              <circle cx="280" cy="80" r="90" stroke-width="0.5" stroke-dasharray="2 3" />
              <circle cx="280" cy="80" r="60" stroke-width="0.5" stroke-dasharray="1 2" />
            </g>
          </svg>
    
          <!-- Brushed Metallic Sheen & Specular Lighting -->
          <div
            class="pointer-events-none absolute inset-0 bg-gradient-to-tr from-transparent via-white/[0.07] to-transparent opacity-80"
          ></div>
          <div
            class={cn(
              'pointer-events-none absolute -inset-y-8 -left-1/3 w-1/2 rotate-12 bg-gradient-to-r opacity-70',
              theme.foilSheen,
            )}
          ></div>
          <!-- Ambient Corner Highlights -->
          <div
            class={cn(
              'pointer-events-none absolute -top-12 -right-12 h-44 w-44 rounded-full bg-gradient-to-br blur-2xl',
              theme.accentGlow,
            )}
          ></div>
          <div class="pointer-events-none absolute -bottom-10 -left-10 h-36 w-36 rounded-full bg-black/40 blur-2xl"></div>
    
          <!-- Outer Bevel Perimeter Ring -->
          <div
            class="pointer-events-none absolute inset-0 rounded-[16px] shadow-[inset_0_1px_1px_rgba(255,255,255,0.35),inset_0_-1px_1px_rgba(0,0,0,0.5)]"
          ></div>
    
          <!-- Shimmer sweep (opt-in) -->
          {#if shimmer}
            <div
              class="pointer-events-none absolute -inset-x-full inset-y-0 bg-gradient-to-r from-transparent via-white/25 to-transparent"
              style="animation: payment-card-shimmer 2.5s linear infinite"
            ></div>
          {/if}
    
          <!-- Content Structure -->
          <div class={cn('absolute inset-0 flex flex-col justify-between', variant === 'compact' ? 'p-2' : 'p-5')}>
            <!-- TOP ROW: Bank Watermark / Card Tier & Brand Logo -->
            <div class="flex items-center justify-between">
              <!-- Left: Card Tier / Bank label -->
              <div class="flex items-center gap-1.5">
                {#if variant !== 'compact'}
                  <span class="font-mono text-xs font-semibold tracking-[0.25em] text-white/70 uppercase">
                    {tierLabel}
                  </span>
                {:else}
                  <span class="font-mono text-xs font-bold tracking-widest text-white/70 uppercase"> CARD </span>
                {/if}
              </div>
    
              <!-- Right: Authentic Brand Logo -->
              <div class="flex items-center justify-end" style="perspective: 600px">
                {#key detectedBrand}
                  <!-- .brand-pop replays the enter animation on every brand change (the Vue <Transition> equivalent). -->
                  <div class="brand-pop">
                    {#if detectedBrand === 'visa'}
                      <!-- VISA -->
                      <div class="flex items-center">
                        <svg
                          class={variant === 'compact' ? 'h-3 w-auto' : 'h-6 w-auto'}
                          viewBox="0 0 78 24"
                          fill="none"
                          xmlns="http://www.w3.org/2000/svg"
                          aria-label="Visa"
                        >
                          <path
                            d="M29.5 23.2H23.9L27.4 1.8H33L29.5 23.2ZM52.4 2.3C51.3 1.9 49.5 1.5 47.3 1.5C41.8 1.5 37.9 4.4 37.8 8.6C37.7 11.7 40.5 13.4 42.6 14.4C44.8 15.5 45.5 16.2 45.5 17.1C45.5 18.6 43.7 19.2 42.1 19.2C39.8 19.2 38.5 18.9 36.8 18.1L36 17.7L35.1 23C36.6 23.7 39.2 24.2 41.8 24.2C47.7 24.2 51.5 21.3 51.6 16.9C51.7 13.4 49.6 11.7 47 10.4C45.2 9.5 44.1 8.9 44.1 8C44.1 7.2 45 6.4 46.8 6.4C48.3 6.4 49.6 6.7 50.8 7.2L51.3 7.5L52.4 2.3ZM67.5 1.8H63.2C61.8 1.8 60.8 2.2 60.2 3.6L51.4 23.2H57.3L58.5 19.9H65.7L66.4 23.2H71.6L67.5 1.8ZM60.1 15.5C60.6 14.2 62.5 8.9 62.5 8.9C62.5 8.9 63 7.6 63.3 6.7L63.8 8.7C63.8 8.7 64.9 14.1 65.2 15.5H60.1ZM18.7 1.8L13.2 16.4L12.6 13.4C11.6 10 8.7 6.3 5.4 4.5L10.3 23.2H16.3L25.1 1.8H18.7ZM8.3 1.8H0.2L0 2.3C6.3 3.9 10.4 7.7 12.1 12.4L10.4 3.7C10.1 2.4 9.3 1.9 8.3 1.8H0.2L0 2.3C6.3 3.9 10.4 7.7 12.1 12.4Z"
                            fill="white"
                          />
                          <path
                            d="M12.1 12.4L10.4 3.7C10.1 2.4 9.3 1.9 8.3 1.8H0.2L0 2.3C6.3 3.9 10.4 7.7 12.1 12.4Z"
                            fill="#F9A01B"
                          />
                        </svg>
                      </div>
                    {:else if detectedBrand === 'mastercard'}
                      <!-- MASTERCARD -->
                      <div class="flex items-center gap-1.5">
                        <svg
                          class={variant === 'compact' ? 'h-3.5 w-auto' : 'h-7 w-auto'}
                          viewBox="0 0 48 30"
                          fill="none"
                          xmlns="http://www.w3.org/2000/svg"
                          aria-label="Mastercard"
                        >
                          <circle cx="15" cy="15" r="15" fill="#EB001B" />
                          <circle cx="33" cy="15" r="15" fill="#F79E1B" fill-opacity="0.92" />
                          <path
                            d="M24 4.5C27.5 7.1 29.8 11.2 29.8 16C29.8 20.8 27.5 24.9 24 27.5C20.5 24.9 18.2 20.8 18.2 16C18.2 11.2 20.5 7.1 24 4.5Z"
                            fill="#FF5F00"
                          />
                        </svg>
                        {#if variant !== 'compact'}
                          <span
                            class="font-mono text-xs font-bold tracking-tight text-white lowercase"
                            style="letter-spacing: -0.05em"
                          >
                            mastercard
                          </span>
                        {/if}
                      </div>
                    {:else if detectedBrand === 'amex'}
                      <!-- AMEX -->
                      <div class="flex items-center">
                        <div
                          class={cn(
                            'flex items-center justify-center rounded-sm bg-[#006fcf] font-sans font-bold tracking-tighter text-white shadow-xs',
                            variant === 'compact' ? 'px-1 py-0.5 text-xs' : 'px-2 py-1 text-xs',
                          )}
                        >
                          AMEX
                        </div>
                      </div>
                    {:else if detectedBrand === 'discover'}
                      <!-- DISCOVER -->
                      <div class="flex items-center gap-0.5 text-xs font-bold tracking-wider text-white">
                        <span>DISC</span>
                        <span
                          class={cn(
                            'inline-block rounded-full bg-gradient-to-br from-amber-400 to-orange-600 shadow-xs',
                            variant === 'compact' ? 'size-2' : 'size-3.5',
                          )}
                        ></span>
                        <span>VER</span>
                      </div>
                    {:else}
                      <!-- UNKNOWN / SLEEK DEFAULT -->
                      <div class="flex items-center gap-1 opacity-80">
                        <div class="size-2 rounded-full bg-white/60"></div>
                        <div class="size-2 rounded-full bg-white/30"></div>
                        <span class="font-mono text-xs font-bold tracking-widest text-white/80 uppercase"> PREMIUM </span>
                      </div>
                    {/if}
                  </div>
                {/key}
              </div>
            </div>
    
            <!-- MIDDLE ROW: EMV Chip + Contactless Symbol -->
            <div class="flex items-center justify-between">
              <div class="flex items-center gap-3">
                <!-- Ultra-Realistic Metallic EMV Chip -->
                <div
                  class={cn(
                    'relative overflow-hidden rounded-[5px] border border-amber-950/40 shadow-[0_2px_5px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.6)]',
                    theme.chipType === 'silver'
                      ? 'bg-gradient-to-br from-slate-200 via-slate-300 to-slate-400'
                      : 'bg-gradient-to-br from-[#ffe082] via-[#ffb300] to-[#ff8f00]',
                    variant === 'compact' ? 'h-3.5 w-5' : 'h-8 w-11',
                  )}
                >
                  <!-- Authentic Chip Circuit Etching Lines -->
                  <div class="absolute inset-[2px] rounded-[3px] border border-black/25"></div>
                  <div class="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-black/30"></div>
                  <div class="absolute inset-y-0 left-1/3 w-px bg-black/30"></div>
                  <div class="absolute inset-y-0 right-1/3 w-px bg-black/30"></div>
                  <div
                    class="absolute top-1/4 right-1/3 left-1/3 h-1/2 rounded-[2px] border border-black/25 bg-black/10"
                  ></div>
                  <!-- Metallic Glint Reflection -->
                  <div
                    class="pointer-events-none absolute inset-0 bg-gradient-to-tr from-transparent via-white/40 to-transparent"
                  ></div>
                </div>
    
                <!-- Contactless NFC Radio Waves -->
                {#if variant !== 'compact'}
                  <svg
                    viewBox="0 0 24 24"
                    class="h-5 w-auto text-white/70 drop-shadow-sm"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="1.75"
                    stroke-linecap="round"
                    aria-hidden="true"
                  >
                    <path d="M6.5 8a6 6 0 0 1 0 8" />
                    <path d="M10 6a9.5 9.5 0 0 1 0 12" />
                    <path d="M13.5 4a13 13 0 0 1 0 16" />
                  </svg>
                {/if}
              </div>
    
              <!-- Holographic Security Stamp (Optional realistic detail) -->
              {#if variant !== 'compact'}
                <div class="pay-card-holo h-7 w-9 rounded-[4px] opacity-75 shadow-xs"></div>
              {/if}
            </div>
    
            <!-- CARD NUMBER (Authentic Embossed Raised Digits) -->
            <div
              class={cn(
                'flex flex-nowrap items-center font-mono font-bold tracking-[0.18em] tabular-nums',
                theme.embossStyle,
                variant === 'compact' ? 'my-0.5 text-xs' : 'my-1 text-base sm:text-lg',
              )}
            >
              <span class="sr-only">{displayNumber}</span>
              {#each displayChars as ch, i (`${i}-${ch}`)}
                <span
                  class={cn(
                    'inline-block origin-bottom tabular-nums transition-transform',
                    ch === '•' && 'text-[0.85em] opacity-50',
                    shouldPop(i, ch) && 'pay-char-pop',
                  )}
                  aria-hidden="true"
                >
                  {ch === ' ' ? ' ' : ch}
                </span>
              {/each}
            </div>
    
            <!-- BOTTOM ROW: Cardholder Name + Expiry Date -->
            <div class="flex items-end justify-between gap-3">
              <!-- Cardholder Name -->
              <div class="min-w-0 flex-1">
                {#if variant !== 'compact'}
                  <div class={cn('text-xs font-semibold tracking-[0.2em] uppercase', theme.subtextColor)}>
                    CARDHOLDER NAME
                  </div>
                {/if}
                <div
                  class={cn(
                    'truncate font-semibold tracking-wider uppercase',
                    theme.embossStyle,
                    variant === 'compact' ? 'text-xs' : 'text-xs',
                  )}
                >
                  {displayName}
                </div>
              </div>
    
              <!-- Expiry Date -->
              <div class="shrink-0 text-right">
                {#if variant !== 'compact'}
                  <div
                    class={cn(
                      'flex items-center justify-end gap-1 text-xs font-semibold tracking-wider uppercase',
                      theme.subtextColor,
                    )}
                  >
                    <span class="text-[6px] leading-tight">GOOD<br />THRU</span>
                    <span>MONTH/YEAR</span>
                  </div>
                {/if}
                <div
                  class={cn(
                    'font-mono font-semibold tracking-wider whitespace-nowrap',
                    theme.embossStyle,
                    variant === 'compact' ? 'text-xs' : 'text-xs',
                  )}
                >
                  {displayExpiry}
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <!-- ================================= BACK FACE ================================= -->
        <div
          class={cn(
            'pay-card-face absolute inset-0 [transform:rotateY(180deg)] overflow-hidden rounded-[16px] border border-white/15 [backface-visibility:hidden]',
            theme.cardClass,
          )}
        >
          <!-- Specular Bevel Perimeter -->
          <div
            class="pointer-events-none absolute inset-0 rounded-[16px] shadow-[inset_0_1px_1px_rgba(255,255,255,0.35),inset_0_-1px_1px_rgba(0,0,0,0.5)]"
          ></div>
    
          <!-- Full-Width Deep Matte Magnetic Stripe -->
          <div
            class={cn(
              'w-full bg-gradient-to-b from-neutral-950 via-black to-neutral-900 shadow-[inset_0_1px_0_rgba(255,255,255,0.1),inset_0_-1px_0_rgba(0,0,0,0.8)]',
              variant === 'compact' ? 'mt-2 h-4' : 'mt-4 h-10',
            )}
          ></div>
    
          <div class={cn('relative flex flex-col justify-between', variant === 'compact' ? 'p-2' : 'p-4')}>
            <!-- Signature Panel + CVV Code -->
            <div class={cn('flex items-stretch gap-2', variant === 'compact' ? 'mt-1' : 'mt-2')}>
              <!-- White Security Hatched Signature Strip -->
              <div
                class={cn(
                  'relative flex-1 overflow-hidden rounded-[3px] border border-slate-300/80 bg-white/95 px-2 py-1',
                  variant === 'compact' ? 'h-4' : 'h-8',
                )}
                style="background-image: repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.05) 0px, rgba(0, 0, 0, 0.05) 2px, transparent 2px, transparent 6px);"
              >
                {#if variant !== 'compact'}
                  <div class="font-mono text-xs font-semibold tracking-wider text-slate-400 uppercase">
                    AUTHORIZED SIGNATURE
                  </div>
                {/if}
              </div>
    
              <!-- Printed CVV Code Box -->
              <div
                class={cn(
                  'flex flex-col items-center justify-center rounded-[3px] border border-slate-300 bg-white font-mono font-bold text-slate-950 shadow-inner',
                  variant === 'compact' ? 'px-1 text-xs' : 'px-3 text-xs',
                )}
              >
                {#if variant !== 'compact'}
                  <span class="text-[6px] font-bold tracking-widest text-slate-400 uppercase"> CVV </span>
                {/if}
                <span class="leading-none tracking-widest italic">{displayCvc}</span>
              </div>
            </div>
    
            <!-- Microtext Disclaimer & Regulatory Compliance Marks -->
            {#if variant !== 'compact'}
              <div class="mt-3 space-y-1.5">
                <p class="text-[7.5px] leading-snug text-white/50">
                  This card is property of the issuing bank and must be returned upon request. Use is subject to the
                  cardholder agreement.
                </p>
                <div
                  class="flex items-center justify-between border-t border-white/10 pt-2 text-xs font-medium text-white/60"
                >
                  <span>24/7 Support: 1-800-555-0199</span>
                  <span class="font-mono font-bold tracking-wider text-white/75">CIRRUS · PLUS · STAR</span>
                </div>
              </div>
            {/if}
          </div>
        </div>
      </div>
    </div>
    
    <style>
      @keyframes payment-card-shimmer {
        0% {
          transform: translateX(-100%);
        }
        100% {
          transform: translateX(200%);
        }
      }
    
      @keyframes pay-char-pop {
        0% {
          opacity: 0;
          transform: translateY(8px) scale(0.6);
        }
        50% {
          opacity: 1;
          transform: translateY(-2px) scale(1.15);
        }
        100% {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }
    
      .pay-char-pop {
        animation: pay-char-pop 360ms cubic-bezier(0.22, 1.4, 0.36, 1) both;
      }
    
      .pay-card-holo {
        background: linear-gradient(
          135deg,
          rgba(255, 255, 255, 0.2) 0%,
          rgba(56, 189, 248, 0.6) 25%,
          rgba(168, 85, 247, 0.6) 50%,
          rgba(251, 146, 60, 0.55) 75%,
          rgba(74, 222, 128, 0.5) 100%
        );
        box-shadow:
          inset 0 0 0 1px rgba(255, 255, 255, 0.35),
          0 2px 6px rgba(0, 0, 0, 0.3);
      }
    
      /* Replays on every brand change via {#key} — the Vue <Transition name="brand-pop"> equivalent. */
      @keyframes brand-pop-in {
        from {
          opacity: 0;
          transform: scale(0.7) rotateY(-90deg);
        }
        to {
          opacity: 1;
          transform: scale(1) rotateY(0deg);
        }
      }
    
      .brand-pop {
        animation: brand-pop-in 300ms cubic-bezier(0.22, 1, 0.36, 1) both;
        transform-style: preserve-3d;
        backface-visibility: hidden;
      }
    
      @media (prefers-reduced-motion: reduce) {
        [data-slot='payment-card'] [class*='transition-'] {
          transition-duration: 0ms !important;
        }
        [data-slot='payment-card'] [style*='payment-card-shimmer'] {
          animation: none !important;
        }
        .pay-char-pop {
          animation: none !important;
        }
        .brand-pop {
          animation: none !important;
        }
      }
    </style>
    
  • src/lib/components/ui/payment-card/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/svelte/payment-card.json