Web Animations Medium
Hover Image List
Typographic project list with a floating preview image that follows the cursor using GSAP quickTo inertia.
Open in Lab
MCP
html css gsap javascript
Targets: JS HTML
Code
[hidden] {
display: none !important;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: system-ui, sans-serif;
background: #faf9f7;
color: #111;
}
.layout {
max-width: 640px;
margin: 0 auto;
padding: 4rem 1.5rem;
}
header h1 {
margin: 0 0 0.35rem;
font-size: 1.5rem;
}
header p {
margin: 0 0 2.5rem;
color: #666;
font-size: 0.9rem;
}
.list {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid #ddd;
}
.list li {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 1.25rem 0;
border-bottom: 1px solid #ddd;
cursor: default;
}
.title {
font-size: clamp(1.25rem, 4vw, 1.75rem);
font-weight: 500;
letter-spacing: -0.02em;
}
.meta {
font-size: 0.75rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.floater {
position: fixed;
top: 0;
left: 0;
width: 220px;
height: 280px;
pointer-events: none;
z-index: 10;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
opacity: 0;
}
.floater img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (max-width: 720px) {
.floater {
display: none !important;
}
}const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const list = document.querySelector("[data-list]");
const floater = document.querySelector("[data-floater]");
const floaterImg = floater.querySelector("img");
if (window.matchMedia("(max-width: 720px)").matches) {
floater.hidden = true;
} else if (!reduced) {
gsap.set(floater, { xPercent: -50, yPercent: -50, scale: 0.92 });
const xTo = gsap.quickTo(floater, "x", { duration: 0.45, ease: "power3.out" });
const yTo = gsap.quickTo(floater, "y", { duration: 0.45, ease: "power3.out" });
function show(src) {
floater.hidden = false;
floaterImg.src = src;
gsap.to(floater, { opacity: 1, scale: 1, duration: 0.35, ease: "power2.out" });
}
function hide() {
gsap.to(floater, {
opacity: 0,
scale: 0.92,
duration: 0.25,
onComplete: () => {
floater.hidden = true;
},
});
}
window.addEventListener("mousemove", (e) => {
xTo(e.clientX);
yTo(e.clientY);
});
list.querySelectorAll("li").forEach((row) => {
row.addEventListener("mouseenter", () => show(row.dataset.img));
row.addEventListener("mouseleave", hide);
});
}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hover Image List</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="layout">
<header>
<h1>Selected work</h1>
<p>Hover a row — preview follows your pointer.</p>
</header>
<ul class="list" data-list>
<li data-img="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=600&q=80">
<span class="title">Aurora UI</span>
<span class="meta">Brand · 2025</span>
</li>
<li data-img="https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=600&q=80">
<span class="title">Kinetic Type</span>
<span class="meta">Web · 2025</span>
</li>
<li data-img="https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=600&q=80">
<span class="title">Chromatic</span>
<span class="meta">Campaign · 2024</span>
</li>
<li data-img="https://images.unsplash.com/photo-1567095761054-7a02e69e5c43?w=600&q=80">
<span class="title">Mono Archive</span>
<span class="meta">Editorial · 2024</span>
</li>
</ul>
</main>
<div class="floater" data-floater hidden aria-hidden="true">
<img src="" alt="" />
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>Hover Image List
A cursor-following preview swaps image sources per row — common on agency index pages without a heavy WebGL stack.