Coupon Card
Original · freeA coupon/ticket card with a perforated edge and promo code.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-coupon.json"use client";
import { useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Coupon = {
id: string;
brand: string;
headline: string;
value: string;
code: string;
terms: string;
expires: string;
accent: "indigo" | "emerald" | "rose" | "amber";
};
const COUPONS: Coupon[] = [
{
id: "aeropress",
brand: "Blue Bottle Coffee",
headline: "New Orleans Iced blend",
value: "25% OFF",
code: "COLDBREW25",
terms: "Valid on whole-bean orders over $30. One use per customer.",
expires: "Aug 31, 2026",
accent: "indigo",
},
{
id: "allbirds",
brand: "Allbirds",
headline: "Tree Runner sneakers",
value: "$20 OFF",
code: "TREE20FLY",
terms: "Applies to full-price footwear. Cannot combine with other offers.",
expires: "Sep 15, 2026",
accent: "emerald",
},
{
id: "moleskine",
brand: "Moleskine Studio",
headline: "Classic hardcover notebooks",
value: "BUY 2 GET 1",
code: "INKWELL3",
terms: "Add three notebooks to cart, discount applies at checkout.",
expires: "Jul 31, 2026",
accent: "rose",
},
];
const ACCENTS: Record<
Coupon["accent"],
{ chip: string; ring: string; text: string; glow: string; btn: string }
> = {
indigo: {
chip: "bg-indigo-500/12 text-indigo-700 dark:bg-indigo-400/15 dark:text-indigo-300",
ring: "focus-visible:ring-indigo-500",
text: "text-indigo-600 dark:text-indigo-300",
glow: "before:bg-indigo-500/40 dark:before:bg-indigo-400/30",
btn: "bg-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 text-white",
},
emerald: {
chip: "bg-emerald-500/12 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300",
ring: "focus-visible:ring-emerald-500",
text: "text-emerald-600 dark:text-emerald-300",
glow: "before:bg-emerald-500/40 dark:before:bg-emerald-400/30",
btn: "bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 text-white",
},
rose: {
chip: "bg-rose-500/12 text-rose-700 dark:bg-rose-400/15 dark:text-rose-300",
ring: "focus-visible:ring-rose-500",
text: "text-rose-600 dark:text-rose-300",
glow: "before:bg-rose-500/40 dark:before:bg-rose-400/30",
btn: "bg-rose-600 hover:bg-rose-500 active:bg-rose-700 text-white",
},
amber: {
chip: "bg-amber-500/15 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300",
ring: "focus-visible:ring-amber-500",
text: "text-amber-600 dark:text-amber-300",
glow: "before:bg-amber-500/40 dark:before:bg-amber-400/30",
btn: "bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-neutral-950",
},
};
function ScissorsIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<circle cx="6" cy="6" r="2.6" />
<circle cx="6" cy="18" r="2.6" />
<path d="M8.2 7.6 20 18M8.2 16.4 20 6M12 12l-3.8 2.6" />
</svg>
);
}
function CopyIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<rect x="9" y="9" width="11" height="11" rx="2.4" />
<path d="M5 15V6a2 2 0 0 1 2-2h8" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.1}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="m5 12.5 4.2 4.2L19 6.5" />
</svg>
);
}
function TicketIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M4 8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2 2 2 0 0 0 0 4 2 2 0 0 1-2 2H6a2 2 0 0 1-2-2 2 2 0 0 0 0-4Z" />
<path d="M14 6v12" strokeDasharray="2 2.4" />
</svg>
);
}
function CouponCard({ coupon }: { coupon: Coupon }) {
const accent = ACCENTS[coupon.accent];
const reduce = useReducedMotion();
const [copied, setCopied] = useState(false);
const [redeemed, setRedeemed] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
async function handleCopy() {
try {
await navigator.clipboard.writeText(coupon.code);
setCopied(true);
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
}
return (
<motion.article
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="cpn-card group relative flex w-full max-w-sm flex-col"
>
{/* main stub */}
<div className="relative overflow-hidden rounded-t-2xl border border-b-0 border-neutral-200 bg-white px-6 pb-7 pt-6 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
<div
className={`pointer-events-none absolute -right-10 -top-10 h-32 w-32 rounded-full opacity-60 blur-2xl [background:radial-gradient(circle,currentColor,transparent_70%)] ${accent.text}`}
aria-hidden="true"
/>
<div className="relative flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span
className={`inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${accent.chip}`}
>
<TicketIcon className="h-4 w-4" />
</span>
<p className="truncate text-[0.7rem] font-semibold uppercase tracking-[0.18em] text-neutral-500 dark:text-neutral-400">
{coupon.brand}
</p>
</div>
<h3 className="mt-3 text-lg font-semibold leading-snug text-neutral-900 dark:text-neutral-50">
{coupon.headline}
</h3>
</div>
<span
className={`shrink-0 rounded-full px-3 py-1 text-xs font-bold tracking-tight ${accent.chip}`}
>
{coupon.value}
</span>
</div>
<p className="relative mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{coupon.terms}
</p>
<div className="relative mt-5 flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
<ScissorsIcon className="h-4 w-4" />
<span>
Expires{" "}
<time className="font-medium text-neutral-700 dark:text-neutral-300">
{coupon.expires}
</time>
</span>
</div>
</div>
{/* perforated divider */}
<div className="relative h-6 border-x border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
<span
className="absolute -left-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full border border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-950"
aria-hidden="true"
/>
<span
className="absolute -right-3 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full border border-neutral-200 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-950"
aria-hidden="true"
/>
<span
className="absolute left-4 right-4 top-1/2 -translate-y-1/2 border-t-2 border-dashed border-neutral-200 dark:border-neutral-700"
aria-hidden="true"
/>
</div>
{/* code stub */}
<div className="relative overflow-hidden rounded-b-2xl border border-t-0 border-neutral-200 bg-white px-6 pb-6 pt-5 shadow-sm dark:border-neutral-800 dark:bg-neutral-900">
<div className="flex items-stretch gap-2">
<div className="relative flex-1 overflow-hidden rounded-xl border border-dashed border-neutral-300 bg-neutral-50 px-4 py-3 dark:border-neutral-700 dark:bg-neutral-950/60">
<p className="text-[0.65rem] font-medium uppercase tracking-[0.2em] text-neutral-400 dark:text-neutral-500">
Promo code
</p>
<p
className={`mt-0.5 font-mono text-lg font-bold tracking-[0.15em] ${
redeemed
? "text-neutral-400 line-through dark:text-neutral-600"
: "text-neutral-900 dark:text-neutral-50"
}`}
>
{coupon.code}
</p>
</div>
<button
type="button"
onClick={handleCopy}
disabled={redeemed}
aria-label={copied ? "Code copied" : `Copy promo code ${coupon.code}`}
className={`relative inline-flex w-14 shrink-0 items-center justify-center rounded-xl border border-neutral-200 bg-neutral-50 text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-neutral-700 dark:bg-neutral-950/60 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:hover:text-white dark:focus-visible:ring-offset-neutral-900 ${accent.ring}`}
>
{copied ? (
<CheckIcon className="h-5 w-5 text-emerald-500" />
) : (
<CopyIcon className="h-5 w-5" />
)}
</button>
</div>
<button
type="button"
onClick={() => setRedeemed((v) => !v)}
aria-pressed={redeemed}
className={`mt-3 inline-flex w-full items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900 ${
redeemed
? "border border-neutral-200 bg-neutral-100 text-neutral-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-400"
: accent.btn
} ${accent.ring}`}
>
{redeemed ? (
<>
<CheckIcon className="h-4 w-4" /> Redeemed
</>
) : (
"Redeem offer"
)}
</button>
<p
className={`mt-2 text-center text-xs transition-opacity ${
copied ? "opacity-100" : "opacity-0"
} ${accent.text}`}
role="status"
aria-live="polite"
>
{copied ? "Copied to clipboard" : " "}
</p>
</div>
</motion.article>
);
}
export default function CardCoupon() {
return (
<section className="relative w-full overflow-hidden bg-neutral-100 px-6 py-20 dark:bg-neutral-950 sm:px-10">
<style>{`
@keyframes cpncoupon_shine {
0% { transform: translateX(-120%) skewX(-18deg); }
100% { transform: translateX(320%) skewX(-18deg); }
}
.cpn-card:hover .cpn-shine { animation: cpncoupon_shine 1.1s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.cpn-card:hover .cpn-shine { animation: none; }
}
`}</style>
{/* soft dotted backdrop */}
<div
className="pointer-events-none absolute inset-0 opacity-[0.35] [background-image:radial-gradient(circle_at_center,rgb(163_163_163)_1px,transparent_1px)] [background-size:22px_22px] dark:opacity-20"
aria-hidden="true"
/>
<div className="relative mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-neutral-200 bg-white px-3 py-1 text-xs font-medium text-neutral-600 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400">
<ScissorsIcon className="h-3.5 w-3.5" />
Clip & save
</span>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
This week’s coupons
</h2>
<p className="mt-3 text-base text-neutral-600 dark:text-neutral-400">
Tap to copy a code, then mark it redeemed once you’ve used it at
checkout.
</p>
</div>
<div className="mt-14 grid justify-items-center gap-8 sm:grid-cols-2 lg:grid-cols-3">
{COUPONS.map((c) => (
<div key={c.id} className="cpn-card relative w-full max-w-sm">
<div className="pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-2xl">
<span className="cpn-shine absolute inset-y-0 left-0 w-1/3 -translate-x-full bg-gradient-to-r from-transparent via-white/40 to-transparent dark:via-white/10" />
</div>
<CouponCard coupon={c} />
</div>
))}
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

