UIPackage

Payment Card

Vue data-display
Edit on GitHub

Animated 3D credit-card visual. Auto-detects brand (Visa, Mastercard, Amex, Discover) from the card number, flips between front and back, and supports opt-in mouse-parallax tilt and a shimmering gradient sweep. Pure presentational — pass typed values in via props.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/payment-card.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/payment-card

Examples

Props

Name Type / Values Default Required
number string '' optional
name string '' optional
expiry string '' optional
cvc string '' optional
brand
'visa''mastercard''amex''discover''unknown''auto'
'auto' optional
flipped boolean false optional
variant
'default''compact'
'default' optional
tilt boolean false optional
shimmer boolean false optional
flip boolean true optional
size
'sm''md''lg'
'md' optional
class string optional

Used by

Files (2)

  • app/components/ui/payment-card/PaymentCard.vue 20.2 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import { cn } from '@/lib/utils'
    
    type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
    
    // Brand wordmark paths (from simpleicons.org, MIT-licensed). Single-path SVGs
    // drawn against a 24x24 viewBox. Fill is set to currentColor so the parent
    // controls color (white on the dark card gradient).
    const BRAND_PATHS: Record<Exclude<CardBrand, 'unknown'>, string> = {
      visa: 'M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z',
      mastercard:
        'M11.343 18.031c.058.049.12.098.181.146-1.177.783-2.59 1.238-4.107 1.238C3.32 19.416 0 16.096 0 12c0-4.095 3.32-7.416 7.416-7.416 1.518 0 2.931.456 4.105 1.238-.06.051-.12.098-.165.15C9.6 7.489 8.595 9.688 8.595 12c0 2.311 1.001 4.51 2.748 6.031zm5.241-13.447c-1.52 0-2.931.456-4.105 1.238.06.051.12.098.165.15C14.4 7.489 15.405 9.688 15.405 12c0 2.31-1.001 4.507-2.748 6.031-.058.049-.12.098-.181.146 1.177.783 2.588 1.238 4.107 1.238C20.68 19.416 24 16.096 24 12c0-4.094-3.32-7.416-7.416-7.416zM12 6.174c-.096.075-.189.15-.28.231C10.156 7.764 9.169 9.765 9.169 12c0 2.236.987 4.236 2.551 5.595.09.08.185.158.28.232.096-.074.189-.152.28-.232 1.563-1.359 2.551-3.359 2.551-5.595 0-2.235-.987-4.236-2.551-5.595-.09-.08-.184-.156-.28-.231z',
      amex: 'M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507',
      discover:
        'M14.58 12a2.023 2.023 0 1 1-2.025-2.023h.002c1.118 0 2.023.906 2.023 2.023zm-5.2-2.001c-1.124 0-2.025.884-2.025 1.99 0 1.118.878 1.984 2.007 1.984.319 0 .593-.063.93-.221v-.873c-.296.297-.559.416-.895.416-.747 0-1.277-.542-1.277-1.312 0-.73.547-1.306 1.243-1.306.354 0 .622.126.93.428v-.873a1.898 1.898 0 0 0-.913-.233zm-3.352 1.545c-.445-.165-.576-.273-.576-.479 0-.239.233-.422.553-.422.222 0 .405.091.598.308l.388-.508a1.665 1.665 0 0 0-1.117-.422c-.673 0-1.186.467-1.186 1.089 0 .524.239.792.936 1.043.291.103.438.171.513.217a.456.456 0 0 1 .222.394c0 .308-.245.536-.576.536-.354 0-.639-.177-.809-.507l-.479.461c.342.502.752.724 1.317.724.771 0 1.311-.513 1.311-1.249-.002-.603-.252-.876-1.095-1.185zM24 10.3a.29.29 0 0 1-.288.291.29.29 0 0 1-.291-.291v-.003A.29.29 0 1 1 24 10.3zm-.059.001a.235.235 0 0 0-.231-.239.234.234 0 0 0-.232.239c0 .132.104.239.232.239a.235.235 0 0 0 .231-.239zM3.472 13.887h.742v-3.803h-.742v3.803zm12.702-1.248l-1.014-2.554h-.81l1.614 3.9h.399l1.643-3.9h-.804l-1.028 2.554zm2.166 1.248h2.104v-.644h-1.362v-1.027h1.312v-.644h-1.312v-.844h1.362v-.644H18.34v3.803zm5.409-3.557l.11.138h-.097l-.094-.13v.13h-.08v-.334h.107c.081 0 .126.036.126.103.001.046-.025.08-.072.093zm-.006-.092c0-.029-.021-.043-.06-.043h-.014v.087h.014c.039 0 .06-.014.06-.044zm-1.228 2.047l1.197 1.602H22.8l-1.027-1.528h-.097v1.528h-.741v-3.803h1.1c.855 0 1.346.411 1.346 1.123 0 .583-.308.965-.866 1.078zm.103-1.038c0-.37-.251-.563-.713-.563h-.228v1.152h.217c.473-.001.724-.207.724-.589zm-19.487.742a1.91 1.91 0 0 1-.69 1.46c-.365.303-.781.439-1.357.439H.001v-3.803H1.09c1.202 0 2.041.781 2.041 1.904zm-.764-.006c0-.364-.154-.718-.411-.947-.245-.222-.536-.308-1.015-.308H.742v2.515h.199c.479 0 .782-.092 1.015-.302.256-.228.411-.593.411-.958z',
    }
    
    // Tight viewBox per brand so the visible glyph fills its slot. The default
    // simpleicons 24x24 canvas leaves a lot of empty space around wordmarks like
    // Visa and Discover, which made them render visibly smaller than the
    // Mastercard / Amex marks at the same nominal size.
    const BRAND_VIEWBOX: Record<Exclude<CardBrand, 'unknown'>, string> = {
      visa: '0 7 24 10',
      mastercard: '0 4 24 16',
      amex: '0 0 24 24',
      discover: '0 8 24 8',
    }
    
    const props = withDefaults(
      defineProps<{
        number?: string
        name?: string
        expiry?: string
        cvc?: string
        brand?: 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown' | 'auto'
        flipped?: boolean
        variant?: 'default' | 'compact'
        tilt?: boolean
        shimmer?: boolean
        flip?: boolean
        size?: 'sm' | 'md' | 'lg'
        class?: string
      }>(),
      {
        number: '',
        name: '',
        expiry: '',
        cvc: '',
        brand: 'auto',
        flipped: false,
        variant: 'default',
        tilt: false,
        shimmer: false,
        flip: true,
        size: 'md',
      },
    )
    
    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'
    }
    
    const detectedBrand = computed<CardBrand>(() =>
      props.brand === 'auto' ? detectBrand(props.number) : (props.brand as CardBrand),
    )
    
    function maskedNumber(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 + ''.repeat(total)).slice(0, total)
      let i = 0
      return groups
        .map((g) => {
          const slice = padded.slice(i, i + g)
          i += g
          return slice
        })
        .join(' ')
    }
    
    const displayNumber = computed(() => maskedNumber(props.number, detectedBrand.value))
    const displayName = computed(() => {
      const fallback = props.variant === 'compact' ? '' : 'CARDHOLDER NAME'
      return (props.name || fallback).toUpperCase()
    })
    const displayExpiry = computed(() => props.expiry || 'MM/YY')
    const displayCvc = computed(() => {
      const want = detectedBrand.value === 'amex' ? 4 : 3
      const v = (props.cvc || '').replace(/\D/g, '').slice(0, want)
      return v.padEnd(want, '')
    })
    
    const brandGradient = computed(() => {
      switch (detectedBrand.value) {
        case 'visa':
          return 'bg-gradient-to-br from-blue-600 via-blue-700 to-blue-900'
        case 'mastercard':
          return 'bg-gradient-to-br from-orange-500 via-red-500 to-red-700'
        case 'amex':
          return 'bg-gradient-to-br from-teal-500 via-teal-600 to-teal-800'
        case 'discover':
          return 'bg-gradient-to-br from-orange-400 via-orange-500 to-orange-700'
        default:
          return 'bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900'
      }
    })
    
    const sizeClass = computed(() => {
      if (props.variant === 'compact') return 'w-[120px]'
      return { sm: 'w-[280px]', md: 'w-[340px]', lg: 'w-[400px]' }[props.size]
    })
    
    const fontClass = computed(() => {
      if (props.variant === 'compact') return 'text-[7px]'
      return { sm: 'text-xs', md: 'text-sm', lg: 'text-base' }[props.size]
    })
    
    const numberFontClass = computed(() => {
      if (props.variant === 'compact') return 'text-[7px] tracking-tight'
      return { sm: 'text-base tracking-wider', md: 'text-lg tracking-wider', lg: 'text-xl tracking-widest' }[props.size]
    })
    
    const brandHeightClass = computed(() => {
      if (props.variant === 'compact') return 'h-2.5'
      return { sm: 'h-4', md: 'h-5', lg: 'h-6' }[props.size]
    })
    
    // Tilt — mouse-parallax
    const cardRef = ref<HTMLDivElement | null>(null)
    const tiltX = ref(0)
    const tiltY = ref(0)
    let rafId: number | null = null
    
    function onMove(e: MouseEvent) {
      if (!props.tilt || !cardRef.value) return
      if (rafId) cancelAnimationFrame(rafId)
      rafId = requestAnimationFrame(() => {
        if (!cardRef.value) return
        const rect = cardRef.value.getBoundingClientRect()
        const dx = (e.clientX - rect.left) / rect.width - 0.5
        const dy = (e.clientY - rect.top) / rect.height - 0.5
        tiltY.value = dx * 16
        tiltX.value = -dy * 16
      })
    }
    function onLeave() {
      tiltX.value = 0
      tiltY.value = 0
    }
    
    const innerTransform = computed(() => {
      const flipDeg = props.flipped ? 180 : 0
      return `rotateX(${tiltX.value}deg) rotateY(${tiltY.value + flipDeg}deg)`
    })
    </script>
    
    <template>
      <div
        ref="cardRef"
        data-uipkge
        data-slot="payment-card"
        :class="cn('group relative inline-block select-none [perspective:1200px]', sizeClass, props.class)"
        @mousemove="onMove"
        @mouseleave="onLeave"
      >
        <div
          class="relative aspect-[1.586/1] w-full transition-transform [transform-style:preserve-3d]"
          :class="flip ? 'duration-[600ms] ease-[cubic-bezier(0.4,0,0.2,1)]' : 'duration-300'"
          :style="{ transform: innerTransform }"
        >
          <!-- FRONT -->
          <div
            :class="
              cn(
                'absolute inset-0 overflow-hidden rounded-[14px] text-white [backface-visibility:hidden]',
                'shadow-[0_10px_30px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_0_rgba(255,255,255,0.15)]',
                brandGradient,
              )
            "
          >
            <!-- Lighting overlay: subtle radial highlight top-left -->
            <div
              class="pointer-events-none absolute inset-0"
              style="background: radial-gradient(120% 80% at 0% 0%, rgba(255, 255, 255, 0.18) 0%, transparent 55%)"
            />
            <!-- Subtle bottom-right darkening -->
            <div
              class="pointer-events-none absolute inset-0"
              style="background: radial-gradient(80% 60% at 100% 100%, rgba(0, 0, 0, 0.25) 0%, transparent 60%)"
            />
            <!-- Shimmer sweep -->
            <div
              v-if="shimmer"
              class="pointer-events-none absolute -inset-x-full inset-y-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
              style="animation: payment-card-shimmer 2.5s linear infinite"
            />
    
            <div :class="cn('absolute inset-0 flex flex-col', variant === 'compact' ? 'p-2' : 'p-4')">
              <!-- Brand top-right -->
              <div class="flex h-auto justify-end text-white">
                <Transition name="brand-fade" mode="out-in">
                  <svg
                    v-if="detectedBrand !== 'unknown'"
                    :key="detectedBrand"
                    :viewBox="BRAND_VIEWBOX[detectedBrand]"
                    :class="[brandHeightClass, 'w-auto']"
                    fill="currentColor"
                    preserveAspectRatio="xMidYMid meet"
                    aria-hidden="true"
                  >
                    <path :d="BRAND_PATHS[detectedBrand]" />
                  </svg>
                  <span
                    v-else
                    key="unknown"
                    class="font-semibold tracking-wider opacity-60"
                    :class="variant === 'compact' ? 'text-[8px]' : 'text-xs'"
                    >CARD</span
                  >
                </Transition>
              </div>
    
              <!-- Chip + contactless NFC, below brand on left -->
              <div :class="cn('flex items-center gap-2', variant === 'compact' ? 'mt-1' : 'mt-3')">
                <!-- EMV chip with 8-contact layout -->
                <div
                  :class="
                    cn(
                      'relative rounded-[4px] bg-gradient-to-br from-amber-100 via-yellow-300 to-yellow-600 shadow-[inset_0_0_4px_rgba(0,0,0,0.25)]',
                      variant === 'compact' ? 'h-3 w-4' : 'h-7 w-10',
                    )
                  "
                >
                  <!-- Outer ring -->
                  <div class="absolute inset-[2px] rounded-[2px] border border-yellow-700/30" />
                  <!-- Horizontal divider -->
                  <div class="absolute inset-x-[2px] top-1/2 h-px -translate-y-px bg-yellow-700/40" />
                  <!-- Vertical dividers -->
                  <div class="absolute inset-y-[2px] left-1/3 w-px bg-yellow-700/40" />
                  <div class="absolute inset-y-[2px] left-2/3 w-px bg-yellow-700/40" />
                </div>
    
                <!-- Contactless NFC waves -->
                <svg
                  v-if="variant !== 'compact'"
                  viewBox="0 0 24 24"
                  class="h-6 w-auto opacity-80"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                  aria-hidden="true"
                >
                  <path d="M7 8.5a6 6 0 0 1 0 7" />
                  <path d="M10 6a9.5 9.5 0 0 1 0 12" />
                  <path d="M13 4a13 13 0 0 1 0 16" />
                </svg>
              </div>
    
              <!-- spacer to push number+bottom down -->
              <div class="flex-1" />
    
              <!-- Number — embossed look via subtle text shadow -->
              <div
                :class="
                  cn(
                    'overflow-hidden font-mono font-semibold whitespace-nowrap',
                    variant === 'compact' ? 'mb-1' : 'mb-3',
                    numberFontClass,
                  )
                "
                style="
                  text-shadow:
                    0 1px 0 rgba(0, 0, 0, 0.18),
                    0 -1px 0 rgba(255, 255, 255, 0.08);
                "
              >
                {{ displayNumber }}
              </div>
    
              <!-- Bottom row: name + expiry -->
              <div class="flex items-end justify-between gap-2">
                <div class="min-w-0 flex-1">
                  <div v-if="variant !== 'compact'" class="text-[9px] font-medium tracking-[0.18em] uppercase opacity-50">
                    Cardholder
                  </div>
                  <div :class="cn('truncate font-semibold tracking-wide whitespace-nowrap uppercase', fontClass)">
                    {{ displayName }}
                  </div>
                </div>
                <div class="shrink-0 text-right">
                  <div v-if="variant !== 'compact'" class="text-[9px] font-medium tracking-[0.18em] uppercase opacity-50">
                    Expires
                  </div>
                  <div :class="cn('font-mono font-semibold tracking-wide whitespace-nowrap', fontClass)">
                    {{ displayExpiry }}
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- BACK -->
          <div
            :class="
              cn(
                'absolute inset-0 [transform:rotateY(180deg)] overflow-hidden rounded-[14px] text-white [backface-visibility:hidden]',
                'shadow-[0_10px_30px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_0_rgba(255,255,255,0.15)]',
                brandGradient,
              )
            "
          >
            <!-- Lighting overlay -->
            <div
              class="pointer-events-none absolute inset-0"
              style="background: radial-gradient(120% 80% at 0% 0%, rgba(255, 255, 255, 0.18) 0%, transparent 55%)"
            />
            <!-- Magstripe -->
            <div
              :class="
                cn(
                  'w-full bg-gradient-to-b from-black via-neutral-900 to-black/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.06),inset_0_-1px_0_rgba(0,0,0,0.5)]',
                  variant === 'compact' ? 'mt-3 h-5' : 'mt-6 h-10',
                )
              "
            />
    
            <div :class="cn('relative', variant === 'compact' ? 'p-2' : 'p-4')">
              <!-- Signature strip + CVC -->
              <div :class="cn('flex items-stretch gap-2', variant === 'compact' ? 'mt-1' : 'mt-2')">
                <!-- Signature strip with diagonal hatching -->
                <div
                  :class="
                    cn(
                      'relative flex-1 overflow-hidden rounded bg-white/95 px-2 py-1.5',
                      variant === 'compact' ? 'h-5' : 'h-9',
                    )
                  "
                  style="
                    background-image: repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.08) 0 2px, transparent 2px 6px);
                    background-color: rgba(248, 248, 248, 0.95);
                  "
                >
                  <div
                    v-if="variant !== 'compact'"
                    class="absolute top-1 right-1.5 text-[7px] tracking-wider text-slate-500/80 uppercase"
                  >
                    Signature
                  </div>
                </div>
                <!-- CVC pill -->
                <div
                  :class="
                    cn(
                      'flex flex-col items-center justify-center rounded bg-white font-mono text-slate-900 shadow-sm',
                      variant === 'compact' ? 'px-1 text-[9px]' : 'px-2.5 text-sm',
                    )
                  "
                >
                  <span v-if="variant !== 'compact'" class="text-[7px] font-medium tracking-wider text-slate-500 uppercase"
                    >CVC</span
                  >
                  <span class="leading-none font-semibold">{{ displayCvc }}</span>
                </div>
              </div>
    
              <div v-if="variant !== 'compact'" class="mt-3 text-[9px] tracking-wide opacity-50">
                Authorized signature. Not valid unless signed. For customer service, see your card issuer.
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <style>
    @keyframes payment-card-shimmer {
      0% {
        transform: translateX(-50%);
      }
      100% {
        transform: translateX(150%);
      }
    }
    
    .brand-fade-enter-active,
    .brand-fade-leave-active {
      transition: opacity 200ms ease;
    }
    .brand-fade-enter-from,
    .brand-fade-leave-to {
      opacity: 0;
    }
    
    @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;
      }
    }
    </style>
  • app/components/ui/payment-card/index.ts 0.1 kB
    export { default as PaymentCard } from './PaymentCard.vue'

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