Web InnoventixFreeCode

Gradient Hover Card

Original · free

A card that reveals a gradient wash and lifts on hover.

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

import { useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";

type Feature = {
  id: string;
  title: string;
  blurb: string;
  gradient: string;
  ring: string;
  icon: ReactNode;
};

type Post = {
  id: string;
  img: string;
  category: string;
  title: string;
  read: string;
  author: string;
  gradient: string;
};

const features: Feature[] = [
  {
    id: "edge",
    title: "Edge Functions",
    blurb: "Deploy TypeScript to 300+ regions. Cold starts under 5ms, billed per request.",
    gradient: "from-indigo-500/25 via-violet-500/15 to-transparent",
    ring: "group-hover:ring-indigo-400/50 dark:group-hover:ring-indigo-400/40",
    icon: (
      <path
        d="M13 2 3 14h7l-1 8 10-12h-7l1-8Z"
        strokeWidth="1.6"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    ),
  },
  {
    id: "vault",
    title: "Secrets Vault",
    blurb: "Encrypted env vars with per-branch scoping and automatic key rotation.",
    gradient: "from-emerald-500/25 via-teal-500/15 to-transparent",
    ring: "group-hover:ring-emerald-400/50 dark:group-hover:ring-emerald-400/40",
    icon: (
      <>
        <rect x="5" y="11" width="14" height="10" rx="2" strokeWidth="1.6" />
        <path d="M8 11V8a4 4 0 0 1 8 0v3" strokeWidth="1.6" strokeLinecap="round" />
      </>
    ),
  },
  {
    id: "insights",
    title: "Live Insights",
    blurb: "Real-time traces, p99 latency and error budgets streamed straight to your dashboard.",
    gradient: "from-rose-500/25 via-amber-500/15 to-transparent",
    ring: "group-hover:ring-rose-400/50 dark:group-hover:ring-rose-400/40",
    icon: (
      <>
        <path d="M4 19V5" strokeWidth="1.6" strokeLinecap="round" />
        <path d="M4 19h16" strokeWidth="1.6" strokeLinecap="round" />
        <path d="M8 15l3-4 3 2 4-6" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
      </>
    ),
  },
];

const post: Post = {
  id: "post",
  img: "/img/gallery/g14.webp",
  category: "Engineering",
  title: "How we cut our build times from 9 minutes to 40 seconds",
  read: "7 min read",
  author: "Priya Nair",
  gradient: "from-sky-500/30 via-indigo-500/20 to-transparent",
};

export default function CardGradientHover() {
  const reduceMotion = useReducedMotion();
  const [liked, setLiked] = useState(false);
  const [likes, setLikes] = useState(248);

  const toggleLike = () => {
    setLiked((prev) => {
      setLikes((n) => (prev ? n - 1 : n + 1));
      return !prev;
    });
  };

  return (
    <section className="relative w-full bg-gradient-to-b from-white to-slate-50 px-5 py-20 dark:from-zinc-950 dark:to-zinc-900 sm:px-8 md:py-28">
      <style>{`
        @keyframes cghShine {
          0% { transform: translateX(-120%) skewX(-12deg); opacity: 0; }
          40% { opacity: 0.75; }
          100% { transform: translateX(240%) skewX(-12deg); opacity: 0; }
        }
        @keyframes cghFloat {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-4px); }
        }
        @media (prefers-reduced-motion: reduce) {
          .cgh-shine, .cgh-float { animation: none !important; }
        }
      `}</style>

      <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-slate-200 bg-white/60 px-3 py-1 text-xs font-medium text-slate-600 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/60 dark:text-zinc-400">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
            Gradient hover cards
          </span>
          <h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-slate-900 dark:text-zinc-50 sm:text-4xl">
            Reveal a wash of color on hover
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-zinc-400">
            Each card lifts, sharpens its border and blooms a soft gradient as your pointer arrives. Tactile, quiet, and fully keyboard-accessible.
          </p>
        </header>

        {/* Feature cards */}
        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {features.map((f, i) => (
            <motion.article
              key={f.id}
              initial={reduceMotion ? false : { opacity: 0, y: 16 }}
              whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
              viewport={{ once: true, margin: "-60px" }}
              transition={{ duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] }}
              className="group relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-sm ring-1 ring-transparent transition-all duration-300 ease-out hover:-translate-y-1.5 hover:shadow-xl hover:shadow-slate-900/5 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-none dark:hover:shadow-black/40 sm:p-7"
            >
              {/* gradient wash */}
              <div
                aria-hidden
                className={`pointer-events-none absolute inset-0 bg-gradient-to-br ${f.gradient} opacity-0 transition-opacity duration-500 ease-out group-hover:opacity-100`}
              />
              {/* shine sweep */}
              <div
                aria-hidden
                className="pointer-events-none absolute inset-0 overflow-hidden opacity-0 transition-opacity duration-300 group-hover:opacity-100"
              >
                <div className="cgh-shine absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/40 to-transparent group-hover:[animation:cghShine_1.1s_ease-out]" />
              </div>

              <div className="relative">
                <span
                  className={`inline-flex h-11 w-11 items-center justify-center rounded-xl bg-slate-100 text-slate-700 ring-1 ring-inset ring-slate-200 transition-colors duration-300 group-hover:bg-white/70 dark:bg-zinc-800 dark:text-zinc-200 dark:ring-zinc-700 dark:group-hover:bg-zinc-800/70`}
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    className="cgh-float h-5 w-5 group-hover:[animation:cghFloat_2.4s_ease-in-out_infinite]"
                    aria-hidden
                  >
                    {f.icon}
                  </svg>
                </span>
                <h3 className="mt-5 text-lg font-semibold text-slate-900 dark:text-zinc-50">
                  {f.title}
                </h3>
                <p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-zinc-400">
                  {f.blurb}
                </p>
                <button
                  type="button"
                  className="mt-5 inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-slate-700 underline-offset-4 transition-colors hover:text-slate-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-zinc-300 dark:hover:text-white dark:focus-visible:ring-zinc-500 dark:focus-visible:ring-offset-zinc-900"
                >
                  Read the docs
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5" aria-hidden>
                    <path d="M5 12h14M13 6l6 6-6 6" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                </button>
              </div>

              {/* border glow on hover via ring */}
              <div
                aria-hidden
                className={`pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-transparent transition-[box-shadow,--tw-ring-color] duration-300 ${f.ring}`}
              />
            </motion.article>
          ))}
        </div>

        {/* Featured photo card */}
        <motion.article
          initial={reduceMotion ? false : { opacity: 0, y: 20 }}
          whileInView={reduceMotion ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-60px" }}
          transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
          className="group relative mt-6 grid grid-cols-1 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition-all duration-300 ease-out hover:-translate-y-1.5 hover:shadow-xl hover:shadow-slate-900/5 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:shadow-black/40 md:grid-cols-2"
        >
          <div
            aria-hidden
            className={`pointer-events-none absolute inset-0 z-10 bg-gradient-to-tr ${post.gradient} opacity-0 transition-opacity duration-500 ease-out group-hover:opacity-100`}
          />

          <div className="relative overflow-hidden">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={post.img}
              alt="Developers reviewing a build pipeline dashboard on a large monitor"
              loading="lazy"
              draggable={false}
              className="h-56 w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.04] md:h-full"
            />
            <div aria-hidden className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent md:bg-gradient-to-r" />
          </div>

          <div className="relative z-20 flex flex-col p-6 sm:p-8">
            <div className="flex items-center gap-3">
              <span className="inline-flex items-center rounded-full bg-indigo-50 px-2.5 py-0.5 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/20">
                {post.category}
              </span>
              <span className="text-xs text-slate-500 dark:text-zinc-500">{post.read}</span>
            </div>

            <h3 className="mt-4 text-balance text-xl font-semibold leading-snug tracking-tight text-slate-900 dark:text-zinc-50 sm:text-2xl">
              {post.title}
            </h3>
            <p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-zinc-400">
              We swapped Webpack for Turbopack, sharded the test suite across the edge, and cached node_modules by lockfile hash. Here is the full teardown.
            </p>

            <div className="mt-auto flex items-center justify-between pt-6">
              <div className="flex items-center gap-3">
                <span className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-600 text-sm font-semibold text-white">
                  PN
                </span>
                <div className="leading-tight">
                  <p className="text-sm font-medium text-slate-800 dark:text-zinc-200">{post.author}</p>
                  <p className="text-xs text-slate-500 dark:text-zinc-500">Staff Engineer</p>
                </div>
              </div>

              <button
                type="button"
                onClick={toggleLike}
                aria-pressed={liked}
                aria-label={liked ? "Remove like" : "Like this article"}
                className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1.5 text-sm font-medium text-slate-700 backdrop-blur transition-all hover:border-rose-300 hover:text-rose-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-95 dark:border-zinc-700 dark:bg-zinc-800/70 dark:text-zinc-300 dark:hover:border-rose-500/40 dark:hover:text-rose-400 dark:focus-visible:ring-offset-zinc-900"
              >
                <svg
                  viewBox="0 0 24 24"
                  className={`h-4 w-4 transition-transform duration-200 ${liked ? "scale-110 fill-rose-500 stroke-rose-500" : "fill-none stroke-current"}`}
                  strokeWidth="1.7"
                  aria-hidden
                >
                  <path d="M12 20s-7-4.35-9.5-8.5C.9 8.7 2.2 5.5 5.3 5.1c1.9-.25 3.4.9 4.7 2.3 1.3-1.4 2.8-2.55 4.7-2.3 3.1.4 4.4 3.6 2.8 6.4C19 15.65 12 20 12 20Z" strokeLinejoin="round" />
                </svg>
                {likes}
              </button>
            </div>
          </div>
        </motion.article>
      </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 →