Web InnoventixFreeCode

Glass Card

Original · free

A frosted glassmorphism card over a soft gradient.

byWeb InnoventixReact + Tailwind
cardglasscards
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-glass.json
card-glass.tsx
"use client";

import { useId, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type CopyButtonProps = {
  value: string;
  label: string;
};

function CopyButton({ value, label }: CopyButtonProps) {
  const [copied, setCopied] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  async function handleCopy() {
    try {
      await navigator.clipboard.writeText(value);
      setCopied(true);
      if (timer.current) clearTimeout(timer.current);
      timer.current = setTimeout(() => setCopied(false), 1600);
    } catch {
      setCopied(false);
    }
  }

  return (
    <button
      type="button"
      onClick={handleCopy}
      aria-label={copied ? "Copied to clipboard" : label}
      className="group inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/50 bg-white/40 text-slate-700 shadow-sm outline-none backdrop-blur transition hover:bg-white/70 focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-95 dark:border-white/10 dark:bg-white/10 dark:text-slate-200 dark:hover:bg-white/20"
    >
      {copied ? (
        <svg viewBox="0 0 24 24" className="h-4 w-4 text-emerald-500" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M20 6 9 17l-5-5" />
        </svg>
      ) : (
        <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <rect x="9" y="9" width="11" height="11" rx="2" />
          <path d="M5 15V5a2 2 0 0 1 2-2h10" />
        </svg>
      )}
    </button>
  );
}

type LikeButtonProps = {
  count: number;
};

function LikeButton({ count }: LikeButtonProps) {
  const [liked, setLiked] = useState(false);
  const reduce = useReducedMotion();

  return (
    <button
      type="button"
      aria-pressed={liked}
      aria-label={liked ? "Remove from favorites" : "Add to favorites"}
      onClick={() => setLiked((v) => !v)}
      className="inline-flex items-center gap-1.5 rounded-full border border-white/50 bg-white/40 px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm outline-none backdrop-blur transition hover:bg-white/70 focus-visible:ring-2 focus-visible:ring-rose-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-95 dark:border-white/10 dark:bg-white/10 dark:text-slate-200 dark:hover:bg-white/20"
    >
      <motion.span
        key={liked ? "on" : "off"}
        initial={reduce ? false : { scale: 0.6 }}
        animate={reduce ? {} : { scale: 1 }}
        transition={{ type: "spring", stiffness: 500, damping: 18 }}
        className="inline-flex"
      >
        <svg
          viewBox="0 0 24 24"
          className={`h-4 w-4 transition-colors ${liked ? "text-rose-500" : "text-slate-500 dark:text-slate-400"}`}
          fill={liked ? "currentColor" : "none"}
          stroke="currentColor"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
          aria-hidden="true"
        >
          <path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 1 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8Z" />
        </svg>
      </motion.span>
      <span className="tabular-nums">{count + (liked ? 1 : 0)}</span>
    </button>
  );
}

type Plan = "monthly" | "annual";

export default function CardGlass() {
  const reduce = useReducedMotion();
  const [plan, setPlan] = useState<Plan>("annual");
  const [expanded, setExpanded] = useState(false);
  const detailsId = useId();

  const price = plan === "annual" ? 24 : 29;

  return (
    <section className="cg-scope relative w-full overflow-hidden px-5 py-20 sm:py-28">
      <style>{`
        @keyframes cg-drift {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(4%, -3%, 0) scale(1.08); }
        }
        @keyframes cg-sheen {
          0% { transform: translateX(-120%); }
          60%, 100% { transform: translateX(220%); }
        }
        .cg-scope .cg-blob { animation: cg-drift 18s ease-in-out infinite; }
        .cg-scope .cg-blob-2 { animation-duration: 24s; animation-direction: reverse; }
        .cg-scope .cg-blob-3 { animation-duration: 30s; }
        .cg-scope .cg-card:hover .cg-sheen { animation: cg-sheen 1.1s ease-out; }
        @media (prefers-reduced-motion: reduce) {
          .cg-scope .cg-blob,
          .cg-scope .cg-card:hover .cg-sheen { animation: none !important; }
        }
      `}</style>

      {/* Soft gradient backdrop */}
      <div className="pointer-events-none absolute inset-0 -z-10 bg-gradient-to-br from-sky-100 via-violet-100 to-rose-100 dark:from-slate-950 dark:via-indigo-950 dark:to-slate-900" />
      <div className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
        <div className="cg-blob absolute -left-24 top-4 h-80 w-80 rounded-full bg-indigo-400/40 blur-3xl dark:bg-indigo-500/25" />
        <div className="cg-blob cg-blob-2 absolute right-0 top-24 h-96 w-96 rounded-full bg-rose-400/40 blur-3xl dark:bg-fuchsia-500/20" />
        <div className="cg-blob cg-blob-3 absolute bottom-0 left-1/3 h-80 w-80 rounded-full bg-sky-400/40 blur-3xl dark:bg-sky-500/20" />
      </div>

      <div className="mx-auto max-w-6xl">
        <header className="mx-auto mb-14 max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-white/50 bg-white/40 px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-600 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Glass UI
          </span>
          <h2 className="mt-4 text-balance text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Frosted glass cards
          </h2>
          <p className="mt-3 text-pretty text-base text-slate-600 dark:text-slate-300">
            Translucent surfaces layered over a soft, drifting gradient. Blur, a
            hairline highlight, and a passing sheen on hover.
          </p>
        </header>

        <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
          {/* Card 1 — Product with photo */}
          <article className="cg-card group relative overflow-hidden rounded-3xl border border-white/50 bg-white/45 p-4 shadow-[0_8px_40px_-12px_rgba(30,41,59,0.35)] backdrop-blur-2xl transition-transform duration-300 hover:-translate-y-1 dark:border-white/10 dark:bg-white/[0.06]">
            <span className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/80 to-transparent dark:via-white/30" />
            <div className="cg-sheen pointer-events-none absolute inset-y-0 -left-1/2 w-1/2 -skew-x-12 bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/10" />

            <div className="relative overflow-hidden rounded-2xl">
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src="/img/gallery/g07.webp"
                alt="Aurora Studio wireless over-ear headphones in graphite"
                loading="lazy"
                draggable={false}
                className="h-44 w-full object-cover"
              />
              <div className="absolute left-3 top-3 rounded-full border border-white/40 bg-white/50 px-2.5 py-1 text-xs font-semibold text-slate-800 backdrop-blur dark:border-white/10 dark:bg-black/30 dark:text-white">
                New
              </div>
            </div>

            <div className="relative mt-4 px-1">
              <div className="flex items-start justify-between gap-3">
                <div>
                  <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
                    Aurora Studio
                  </h3>
                  <p className="text-sm text-slate-600 dark:text-slate-300">
                    Adaptive noise-cancelling headphones
                  </p>
                </div>
                <p className="whitespace-nowrap text-lg font-semibold text-slate-900 dark:text-white">
                  $349
                </p>
              </div>

              <div className="mt-5 flex items-center justify-between">
                <LikeButton count={128} />
                <div className="flex items-center gap-2">
                  <CopyButton value="https://shop.example.com/aurora-studio" label="Copy product link" />
                  <button
                    type="button"
                    className="inline-flex items-center gap-1.5 rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm outline-none transition hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-95 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100"
                  >
                    Add
                  </button>
                </div>
              </div>
            </div>
          </article>

          {/* Card 2 — Pricing with real toggle */}
          <article className="cg-card group relative overflow-hidden rounded-3xl border border-white/50 bg-white/45 p-6 shadow-[0_8px_40px_-12px_rgba(30,41,59,0.35)] backdrop-blur-2xl transition-transform duration-300 hover:-translate-y-1 dark:border-white/10 dark:bg-white/[0.06]">
            <span className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/80 to-transparent dark:via-white/30" />
            <div className="cg-sheen pointer-events-none absolute inset-y-0 -left-1/2 w-1/2 -skew-x-12 bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/10" />

            <div className="relative">
              <div className="flex items-center justify-between">
                <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
                  Pro plan
                </h3>
                <span className="rounded-full border border-indigo-300/50 bg-indigo-500/10 px-2.5 py-1 text-xs font-semibold text-indigo-700 dark:border-indigo-400/30 dark:text-indigo-300">
                  Save 17%
                </span>
              </div>

              <div
                role="group"
                aria-label="Billing period"
                className="mt-4 inline-flex rounded-full border border-white/50 bg-white/40 p-1 backdrop-blur dark:border-white/10 dark:bg-white/10"
              >
                {(["monthly", "annual"] as const).map((option) => {
                  const active = plan === option;
                  return (
                    <button
                      key={option}
                      type="button"
                      aria-pressed={active}
                      onClick={() => setPlan(option)}
                      className={`rounded-full px-4 py-1.5 text-sm font-medium capitalize outline-none transition focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-1 focus-visible:ring-offset-transparent ${
                        active
                          ? "bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-900"
                          : "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white"
                      }`}
                    >
                      {option}
                    </button>
                  );
                })}
              </div>

              <div className="mt-6 flex items-end gap-1">
                <motion.span
                  key={price}
                  initial={reduce ? false : { y: 8, opacity: 0 }}
                  animate={reduce ? {} : { y: 0, opacity: 1 }}
                  transition={{ duration: 0.25, ease: "easeOut" }}
                  className="text-5xl font-semibold tracking-tight text-slate-900 dark:text-white"
                >
                  ${price}
                </motion.span>
                <span className="mb-1.5 text-sm text-slate-500 dark:text-slate-400">
                  / user / mo
                </span>
              </div>

              <ul className="mt-5 space-y-2.5 text-sm text-slate-700 dark:text-slate-300">
                {["Unlimited projects", "Advanced analytics", "Priority support"].map((f) => (
                  <li key={f} className="flex items-center gap-2.5">
                    <svg viewBox="0 0 24 24" className="h-4 w-4 shrink-0 text-emerald-500" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      <path d="M20 6 9 17l-5-5" />
                    </svg>
                    {f}
                  </li>
                ))}
              </ul>

              <button
                type="button"
                className="mt-6 w-full rounded-2xl bg-gradient-to-r from-indigo-500 to-violet-500 px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 outline-none transition hover:from-indigo-600 hover:to-violet-600 focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-[0.98]"
              >
                Start free trial
              </button>
            </div>
          </article>

          {/* Card 3 — Profile with expandable bio */}
          <article className="cg-card group relative overflow-hidden rounded-3xl border border-white/50 bg-white/45 p-6 shadow-[0_8px_40px_-12px_rgba(30,41,59,0.35)] backdrop-blur-2xl transition-transform duration-300 hover:-translate-y-1 dark:border-white/10 dark:bg-white/[0.06]">
            <span className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/80 to-transparent dark:via-white/30" />
            <div className="cg-sheen pointer-events-none absolute inset-y-0 -left-1/2 w-1/2 -skew-x-12 bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/10" />

            <div className="relative">
              <div className="flex items-center gap-4">
                <div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-2xl ring-2 ring-white/60 dark:ring-white/10">
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src="/img/gallery/g19.webp"
                    alt="Portrait of Priya Nair"
                    loading="lazy"
                    draggable={false}
                    className="h-full w-full object-cover"
                  />
                </div>
                <div>
                  <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
                    Priya Nair
                  </h3>
                  <p className="text-sm text-slate-600 dark:text-slate-300">
                    Head of Design, Northwind
                  </p>
                </div>
              </div>

              <p className="mt-4 text-sm leading-relaxed text-slate-700 dark:text-slate-300">
                &ldquo;The frosted panels shipped in a day and lifted trial
                sign-ups by 12%.&rdquo;
              </p>

              <AnimatePresence initial={false}>
                {expanded && (
                  <motion.div
                    id={detailsId}
                    key="details"
                    initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
                    animate={reduce ? { opacity: 1 } : { height: "auto", opacity: 1 }}
                    exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
                    transition={{ duration: 0.28, ease: "easeInOut" }}
                    className="overflow-hidden"
                  >
                    <p className="pt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                      Priya leads a team of nine across Bengaluru and Berlin.
                      Before Northwind she built the design system at Loop and
                      spoke on glassmorphism at Config 2025.
                    </p>
                  </motion.div>
                )}
              </AnimatePresence>

              <div className="mt-5 flex items-center justify-between">
                <button
                  type="button"
                  aria-expanded={expanded}
                  aria-controls={detailsId}
                  onClick={() => setExpanded((v) => !v)}
                  className="inline-flex items-center gap-1.5 text-sm font-semibold text-indigo-700 outline-none transition hover:text-indigo-800 focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent dark:text-indigo-300 dark:hover:text-indigo-200"
                >
                  {expanded ? "Show less" : "Read more"}
                  <svg viewBox="0 0 24 24" className={`h-4 w-4 transition-transform ${expanded ? "rotate-180" : ""}`} fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="m6 9 6 6 6-6" />
                  </svg>
                </button>

                <div className="flex items-center gap-2">
                  <a
                    href="https://www.linkedin.com/"
                    target="_blank"
                    rel="noopener"
                    aria-label="Priya Nair on LinkedIn"
                    className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/50 bg-white/40 text-slate-700 shadow-sm outline-none backdrop-blur transition hover:bg-white/70 focus-visible:ring-2 focus-visible:ring-sky-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-95 dark:border-white/10 dark:bg-white/10 dark:text-slate-200 dark:hover:bg-white/20"
                  >
                    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
                      <path d="M4.98 3.5A2.5 2.5 0 1 1 5 8.5a2.5 2.5 0 0 1-.02-5ZM3 9h4v12H3V9Zm6 0h3.8v1.7h.05c.53-1 1.83-2.05 3.76-2.05C20.4 8.65 22 10.7 22 14v7h-4v-6.2c0-1.48-.03-3.38-2.06-3.38-2.06 0-2.38 1.6-2.38 3.27V21H9V9Z" />
                    </svg>
                  </a>
                  <LikeButton count={64} />
                </div>
              </div>
            </div>
          </article>
        </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 →