html|css|javascript 44 views

Interactive Card Hover Effect with Animation

Enhance your web design with a stylish card hover effect that animates on mouseover, perfect for showcasing portfolios.

By TWC Team • Jan 25, 2026

Code

<!--
Interactive Card Hover Effect with Animation
Drop this snippet into any page to create responsive, accessible cards with smooth hover + pointer-based tilt.
-->
<div class="cards" aria-label="Featured projects">
  <article class="card" tabindex="0">
    <h2>Project Title</h2>
    <p>Description of the project that showcases skills.</p>
  </article>

  <article class="card" tabindex="0">
    <h2>Another Project</h2>
    <p>A second card to demonstrate reusability and layout.</p>
  </article>
</div>

<style>
  :root{
    --card-bg:#fff; --text:#111827; --muted:#4b5563;
    --shadow:0 4px 10px rgba(0,0,0,.18);
    --shadow-hover:0 12px 28px rgba(0,0,0,.22);
    --radius:14px;
  }
  .cards{display:flex;flex-wrap:wrap;gap:20px;padding:20px;justify-content:center;background:#f6f7fb}
  .card{
    width:min(320px,92vw); padding:20px 22px; border-radius:var(--radius);
    background:var(--card-bg); color:var(--text); box-shadow:var(--shadow);
    transform:translateY(0) perspective(900px) rotateX(0) rotateY(0);
    transition:transform .28s ease, box-shadow .28s ease;
    will-change:transform; /* Performance: hints GPU acceleration for smoother animations */
    outline:none; position:relative; overflow:hidden;
  }
  .card h2{margin:0 0 6px;font:600 1.05rem/1.25 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto}
  .card p{margin:0;color:var(--muted);font:400 .95rem/1.45 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto}
  .card::after{
    content:""; position:absolute; inset:-40%; background:radial-gradient(circle at var(--gx,50%) var(--gy,50%), rgba(99,102,241,.22), transparent 55%);
    opacity:0; transition:opacity .28s ease; pointer-events:none;
  }
  .card:hover,.card:focus-visible{transform:translateY(-10px) perspective(900px) rotateX(var(--rx,0)) rotateY(var(--ry,0)); box-shadow:var(--shadow-hover)}
  .card:hover::after,.card:focus-visible::after{opacity:1}
  @media (prefers-reduced-motion: reduce){
    .card{transition:none; transform:none !important}
    .card::after{transition:none}
  }
</style>

<script>
/* Adds optional pointer-based tilt + glow tracking. Uses rAF to avoid excessive layout/paint work. */
(() => {
  const cards = document.querySelectorAll('.card');
  if (!cards.length) return;

  const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
  const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;

  cards.forEach((card) => {
    if (prefersReduced) return;

    let rafId = 0;
    const reset = () => {
      cancelAnimationFrame(rafId);
      card.style.removeProperty('--rx');
      card.style.removeProperty('--ry');
      card.style.removeProperty('--gx');
      card.style.removeProperty('--gy');
    };

    const onMove = (e) => {
      const rect = card.getBoundingClientRect();
      const x = (e.clientX ?? (e.touches && e.touches[0]?.clientX)) - rect.left;
      const y = (e.clientY ?? (e.touches && e.touches[0]?.clientY)) - rect.top;
      if (!Number.isFinite(x) || !Number.isFinite(y) || rect.width === 0 || rect.height === 0) return;

      cancelAnimationFrame(rafId);
      rafId = requestAnimationFrame(() => {
        const px = x / rect.width, py = y / rect.height;
        const ry = clamp((px - 0.5) * 10, -8, 8);   // rotateY
        const rx = clamp((0.5 - py) * 10, -8, 8);   // rotateX
        card.style.setProperty('--rx', `${rx}deg`);
        card.style.setProperty('--ry', `${ry}deg`);
        card.style.setProperty('--gx', `${Math.round(px * 100)}%`);
        card.style.setProperty('--gy', `${Math.round(py * 100)}%`);
      });
    };

    card.addEventListener('pointermove', onMove, { passive: true });
    card.addEventListener('pointerleave', reset);
    card.addEventListener('blur', reset);
  });
})();

/* Example usage:
   Duplicate <article class="card">...</article> blocks, or generate them dynamically from data. */
</script>
Back to Snippets