Payment Card
payment-card ui 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.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/payment-card.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/payment-card.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/payment-card.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/payment-card.json Named registry:
npx shadcn-vue@latest add @uipkge/payment-card Installs to: app/components/ui/payment-card/ Examples
Loading interactive previews…
Loading interactive previews…
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 installed (2)
-
app/components/ui/payment-card/PaymentCard.vue 26.5 kB
<script setup lang="ts"> import { computed, onUnmounted, ref, watch } 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. 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. 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', } // Premium metallic faces per brand — deep color, white embossed type, gold chip. const BRAND_THEME: Record< CardBrand, { gradient: string; logo: string; accent: string; sheen: string; glow: string } > = { visa: { gradient: 'bg-[linear-gradient(145deg,#0b1f4d_0%,#1a4fb8_42%,#0c2d7a_72%,#071536_100%)]', logo: 'text-white', accent: 'from-sky-300/40 via-indigo-400/20 to-transparent', sheen: 'from-white/25 via-sky-200/10 to-transparent', glow: 'shadow-[0_18px_40px_-14px_rgba(30,64,175,0.65),0_8px_16px_-10px_rgba(0,0,0,0.45),inset_0_1px_0_0_rgba(255,255,255,0.28)]', }, mastercard: { gradient: 'bg-[linear-gradient(145deg,#3b0a12_0%,#b91c1c_38%,#c2410c_68%,#450a0a_100%)]', logo: 'text-white', accent: 'from-orange-300/35 via-red-400/20 to-transparent', sheen: 'from-white/22 via-orange-200/10 to-transparent', glow: 'shadow-[0_18px_40px_-14px_rgba(185,28,28,0.55),0_8px_16px_-10px_rgba(0,0,0,0.45),inset_0_1px_0_0_rgba(255,255,255,0.25)]', }, amex: { gradient: 'bg-[linear-gradient(145deg,#042f2e_0%,#0f766e_40%,#115e59_70%,#022c22_100%)]', logo: 'text-white', accent: 'from-teal-200/40 via-emerald-300/15 to-transparent', sheen: 'from-white/22 via-teal-100/10 to-transparent', glow: 'shadow-[0_18px_40px_-14px_rgba(13,148,136,0.55),0_8px_16px_-10px_rgba(0,0,0,0.45),inset_0_1px_0_0_rgba(255,255,255,0.25)]', }, discover: { gradient: 'bg-[linear-gradient(145deg,#431407_0%,#c2410c_40%,#b45309_70%,#292524_100%)]', logo: 'text-white', accent: 'from-amber-200/40 via-orange-300/15 to-transparent', sheen: 'from-white/22 via-amber-100/10 to-transparent', glow: 'shadow-[0_18px_40px_-14px_rgba(194,65,12,0.55),0_8px_16px_-10px_rgba(0,0,0,0.45),inset_0_1px_0_0_rgba(255,255,255,0.25)]', }, unknown: { gradient: 'bg-[linear-gradient(145deg,#0f172a_0%,#334155_45%,#1e293b_75%,#020617_100%)]', logo: 'text-white/90', accent: 'from-slate-200/30 via-slate-400/10 to-transparent', sheen: 'from-white/18 via-slate-200/8 to-transparent', glow: 'shadow-[0_18px_40px_-14px_rgba(15,23,42,0.7),0_8px_16px_-10px_rgba(0,0,0,0.5),inset_0_1px_0_0_rgba(255,255,255,0.2)]', }, } 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 theme = computed(() => BRAND_THEME[detectedBrand.value]) // Per-digit pop: bump a generation key when a digit changes so the span remounts // and the CSS animation always restarts (class-toggle alone is flaky under rapid input). const displayChars = computed(() => [...displayNumber.value]) const charGen = ref<number[]>([]) const previousDisplay = ref(displayNumber.value) const isFirstDisplay = ref(true) watch( displayNumber, (next) => { const nextChars = [...next] if (isFirstDisplay.value) { isFirstDisplay.value = false previousDisplay.value = next charGen.value = nextChars.map(() => 0) return } const prev = previousDisplay.value const gens = nextChars.map((ch, i) => { const prevGen = charGen.value[i] ?? 0 // Animate real digits that just appeared or changed (not spaces / mask bullets). if (ch !== ' ' && ch !== '•' && prev[i] !== ch) return prevGen + 1 return prevGen }) previousDisplay.value = next charGen.value = gens }, { immediate: true }, ) function charKey(i: number, ch: string) { return `${i}-${ch}-${charGen.value[i] ?? 0}` } function shouldPop(i: number, ch: string) { return ch !== ' ' && ch !== '•' && (charGen.value[i] ?? 0) > 0 } 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)` }) onUnmounted(() => { if (rafId) cancelAnimationFrame(rafId) }) </script> <template> <div ref="cardRef" data-uipkge data-slot="payment-card" role="img" :aria-label="`${detectedBrand === 'unknown' ? 'Card' : detectedBrand} ending ${displayNumber.replace(/[•\s]/g, '').slice(-4) || '••••'}, ${displayName}, expires ${displayExpiry}`" :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-[700ms] ease-[cubic-bezier(0.22,1,0.36,1)]' : 'duration-300'" :style="{ transform: innerTransform }" > <!-- FRONT --> <div :class=" cn( 'pay-card-face absolute inset-0 overflow-hidden rounded-[18px] [backface-visibility:hidden]', theme.gradient, theme.glow, ) " > <!-- Fine grain noise --> <div class="pay-card-noise pointer-events-none absolute inset-0 opacity-[0.18] mix-blend-overlay" /> <!-- Corner accent bloom --> <div :class="cn('pointer-events-none absolute -top-10 -right-8 h-40 w-40 rounded-full bg-gradient-to-br blur-2xl', theme.accent)" /> <div class="pointer-events-none absolute -bottom-12 -left-10 h-36 w-36 rounded-full bg-black/30 blur-2xl" /> <!-- Specular top edge --> <div class="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/50 to-transparent" /> <!-- Diagonal foil sheen --> <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.sheen, ) " /> <!-- Soft lighting --> <div class="pointer-events-none absolute inset-0" style="background: radial-gradient(120% 80% at 0% 0%, rgba(255, 255, 255, 0.22) 0%, transparent 55%)" /> <div class="pointer-events-none absolute inset-0" style="background: radial-gradient(80% 60% at 100% 100%, rgba(0, 0, 0, 0.35) 0%, transparent 60%)" /> <!-- Holographic strip --> <div v-if="variant !== 'compact'" class="pay-card-holo pointer-events-none absolute top-[42%] right-3 h-8 w-11 rounded-sm opacity-80" /> <!-- Shimmer sweep (opt-in) --> <div v-if="shimmer" class="pointer-events-none absolute -inset-x-full inset-y-0 bg-gradient-to-r from-transparent via-white/30 to-transparent" style="animation: payment-card-shimmer 2.5s linear infinite" /> <div :class="cn('absolute inset-0 flex flex-col text-white', variant === 'compact' ? 'p-2' : 'p-4')"> <!-- Brand top-right --> <div class="flex h-auto items-center justify-end drop-shadow-[0_1px_2px_rgba(0,0,0,0.35)]" style="perspective: 600px"> <Transition name="brand-pop" mode="out-in"> <svg v-if="detectedBrand !== 'unknown'" :key="detectedBrand" :viewBox="BRAND_VIEWBOX[detectedBrand]" :class="[brandHeightClass, 'pay-brand-mark w-auto', theme.logo]" fill="currentColor" preserveAspectRatio="xMidYMid meet" aria-hidden="true" > <path :d="BRAND_PATHS[detectedBrand]" /> </svg> <span v-else key="unknown" class="pay-brand-mark font-semibold tracking-[0.2em] opacity-80" :class="[variant === 'compact' ? 'text-[8px]' : 'text-xs', theme.logo]" >CARD</span > </Transition> </div> <!-- Chip + contactless NFC --> <div :class="cn('flex items-center gap-2.5', variant === 'compact' ? 'mt-1' : 'mt-3')"> <!-- EMV chip — metallic gold --> <div :class=" cn( 'pay-card-chip relative rounded-[5px]', variant === 'compact' ? 'h-3 w-4' : 'h-8 w-11', ) " > <div class="absolute inset-[2px] rounded-[3px] border border-amber-950/25" /> <div class="absolute inset-x-[3px] top-1/2 h-px -translate-y-px bg-amber-950/30" /> <div class="absolute inset-y-[3px] left-1/3 w-px bg-amber-950/30" /> <div class="absolute inset-y-[3px] left-2/3 w-px bg-amber-950/30" /> </div> <!-- Contactless NFC waves --> <svg v-if="variant !== 'compact'" viewBox="0 0 24 24" class="h-6 w-auto text-white/75 drop-shadow-sm" fill="none" stroke="currentColor" stroke-width="1.6" 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 --> <div class="flex-1" /> <!-- Number — embossed mono --> <div :class=" cn( 'pay-card-emboss flex flex-nowrap overflow-visible font-mono font-semibold leading-none tracking-wider', variant === 'compact' ? 'mb-1' : 'mb-3', numberFontClass, ) " > <span class="sr-only">{{ displayNumber }}</span> <span v-for="(ch, i) in displayChars" :key="charKey(i, ch)" :class=" cn( 'inline-block origin-bottom tabular-nums will-change-transform', ch === '•' && 'opacity-45', shouldPop(i, ch) && 'pay-char-pop', ) " aria-hidden="true" > {{ ch === ' ' ? '\u00A0' : ch }} </span> </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.2em] text-white/55 uppercase" > Cardholder </div> <div :class=" cn( 'pay-card-emboss 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.2em] text-white/55 uppercase" > Expires </div> <div :class="cn('pay-card-emboss font-mono font-semibold tracking-wide whitespace-nowrap', fontClass)"> {{ displayExpiry }} </div> </div> </div> </div> </div> <!-- BACK --> <div :class=" cn( 'pay-card-face absolute inset-0 [transform:rotateY(180deg)] overflow-hidden rounded-[18px] [backface-visibility:hidden]', theme.gradient, theme.glow, ) " > <div class="pay-card-noise pointer-events-none absolute inset-0 opacity-[0.16] mix-blend-overlay" /> <div class="pointer-events-none absolute inset-0" style="background: radial-gradient(120% 80% at 0% 0%, rgba(255, 255, 255, 0.16) 0%, transparent 55%)" /> <!-- Magstripe --> <div :class=" cn( 'w-full bg-gradient-to-b from-neutral-950 via-neutral-900 to-black shadow-[inset_0_1px_0_rgba(255,255,255,0.08),inset_0_-1px_0_rgba(0,0,0,0.55)]', variant === 'compact' ? 'mt-3 h-5' : 'mt-6 h-10', ) " /> <div :class="cn('relative text-white', variant === 'compact' ? 'p-2' : 'p-4')"> <!-- Signature strip + CVC --> <div :class="cn('flex items-stretch gap-2', variant === 'compact' ? 'mt-1' : 'mt-2')"> <div :class="cn('relative flex-1 overflow-hidden rounded-sm px-2 py-1.5', variant === 'compact' ? 'h-5' : 'h-9')" style=" background-image: repeating-linear-gradient(135deg, rgba(0, 0, 0, 0.06) 0 2px, transparent 2px 6px); background-color: rgba(248, 250, 252, 0.96); " > <div v-if="variant !== 'compact'" class="absolute top-1 right-1.5 text-[7px] tracking-wider text-slate-400 uppercase" > Signature </div> </div> <div :class=" cn( 'flex flex-col items-center justify-center rounded-sm bg-white font-mono text-slate-900 shadow-[0_2px_8px_rgba(0,0,0,0.25)]', 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-400 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 text-white/45"> 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%); } } @keyframes pay-char-pop { 0% { opacity: 0; transform: translateY(10px) scale(0.55); } 55% { opacity: 1; transform: translateY(-3px) scale(1.18); } 100% { opacity: 1; transform: translateY(0) scale(1); } } .pay-char-pop { animation: pay-char-pop 380ms cubic-bezier(0.22, 1.4, 0.36, 1) both; } .pay-card-face { border: 1px solid rgba(255, 255, 255, 0.12); } .pay-card-noise { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-size: 140px 140px; } .pay-card-chip { background: linear-gradient(145deg, #fde68a 0%, #f59e0b 35%, #b45309 70%, #fbbf24 100%); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55), inset 0 -1px 0 rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.35); } .pay-card-holo { background: linear-gradient( 125deg, rgba(255, 255, 255, 0.15) 0%, rgba(125, 211, 252, 0.55) 20%, rgba(196, 181, 253, 0.55) 40%, rgba(253, 186, 116, 0.5) 60%, rgba(134, 239, 172, 0.45) 80%, rgba(255, 255, 255, 0.2) 100% ); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25), 0 2px 6px rgba(0, 0, 0, 0.25); } .pay-card-emboss { text-shadow: 0 1px 0 rgba(255, 255, 255, 0.22), 0 -1px 0 rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.25); } /* Brand mark needs a block box so rotateY actually reads. */ .pay-brand-mark { display: block; transform-origin: center center; transform-style: preserve-3d; backface-visibility: hidden; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35)); } .brand-pop-enter-active, .brand-pop-leave-active { transition: opacity 320ms cubic-bezier(0.22, 1, 0.36, 1), transform 320ms cubic-bezier(0.22, 1, 0.36, 1); transform-style: preserve-3d; backface-visibility: hidden; } .brand-pop-enter-from { opacity: 0; transform: scale(0.65) rotateY(-95deg); } .brand-pop-leave-to { opacity: 0; transform: scale(0.65) rotateY(95deg); } @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-enter-active, .brand-pop-leave-active { transition-duration: 0ms !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