Web InnoventixFreeCode

Coupon Card

Original · free

A coupon/ticket card with a perforated edge and promo code.

byWeb InnoventixReact + Tailwind
cardcouponcards
Open

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
card-coupon.tsx
"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 &amp; save
          </span>
          <h2 className="mt-4 text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
            This week&rsquo;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&rsquo;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

motion

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 quote

Similar components

Browse all →