Interactive Card Hover Effect with Animation
Enhance your web design with a stylish card hover effect that animates on mouseover, perfect for showcasing portfolios.
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>