Image Preview Cursor Follower
Documentation
Webflow
Code
Setup: External Scripts
HTML
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>Step 1: Add HTML
HTML
<div data-follower-wrap="" class="preview-container">
<div class="preview-item__row tablet--hide">
<div class="preview-item__col is--large"><span class="preview-container__label">Name</span></div>
<div class="preview-item__col is--small"><span class="preview-container__label">Location</span></div>
<div class="preview-item__col is--small"><span class="preview-container__label">Year</span></div>
<div class="preview-item__col is--medium"><span class="preview-container__label">Services</span></div>
</div>
<div data-follower-collection="" class="preview-collection">
<div class="preview-list">
<div data-follower-item="" class="preview-item">
<a href="#" class="preview-item__inner w-inline-block">
<div class="preview-item__row">
<div class="preview-item__col is--large">
<h2 class="preview-item__heading">Prism</h2>
</div>
<div class="preview-item__col is--small tablet--hide">
<p class="preview-item__text">Belgium</p>
</div>
<div class="preview-item__col is--small">
<p class="preview-item__text">2025</p>
</div>
<div class="preview-item__col is--medium">
<p class="preview-item__text">Development</p>
</div>
</div>
<div data-follower-visual="" class="preview-item__visual">
<img src="https://cdn.prod.website-files.com/6889f182607452ec007a0ae1/688a1e49a704afe5e3f4a55d_Fluid%20Abstract%20Design.avif" class="preview-item__visual-img">
</div>
</a>
</div>
<div data-follower-item="" class="preview-item">
<a href="#" class="preview-item__inner w-inline-block">
<div class="preview-item__row">
<div class="preview-item__col is--large">
<h2 class="preview-item__heading">Oracle</h2>
</div>
<div class="preview-item__col is--small tablet--hide">
<p class="preview-item__text">Australia</p>
</div>
<div class="preview-item__col is--small">
<p class="preview-item__text">2025</p>
</div>
<div class="preview-item__col is--medium">
<p class="preview-item__text">Design, Development</p>
</div>
</div>
<div data-follower-visual="" class="preview-item__visual">
<img src="https://cdn.prod.website-files.com/6889f182607452ec007a0ae1/688a1e2ea2b1de5d693cf173_Elegant%20Ice%20Bottle%20Display.avif" class="preview-item__visual-img">
</div>
</a>
</div>
<div data-follower-item="" class="preview-item">
<a href="#" class="preview-item__inner w-inline-block">
<div class="preview-item__row">
<div class="preview-item__col is--large">
<h2 class="preview-item__heading">Mosaic</h2>
</div>
<div class="preview-item__col is--small tablet--hide">
<p class="preview-item__text">Spain</p>
</div>
<div class="preview-item__col is--small">
<p class="preview-item__text">2024</p>
</div>
<div class="preview-item__col is--medium">
<p class="preview-item__text">Development</p>
</div>
</div>
<div data-follower-visual="" class="preview-item__visual">
<img src="https://cdn.prod.website-files.com/6889f182607452ec007a0ae1/688a1e2e3a3b6987bbb92dfd_Serene%20Floral%20Arrangement.avif" class="preview-item__visual-img">
</div>
</a>
</div>
<div data-follower-item="" class="preview-item">
<a href="#" class="preview-item__inner w-inline-block">
<div class="preview-item__row">
<div class="preview-item__col is--large">
<h2 class="preview-item__heading">Zenith</h2>
</div>
<div class="preview-item__col is--small tablet--hide">
<p class="preview-item__text">Japan</p>
</div>
<div class="preview-item__col is--small">
<p class="preview-item__text">2024</p>
</div>
<div class="preview-item__col is--medium">
<p class="preview-item__text">Strategy, Design</p>
</div>
</div>
<div data-follower-visual="" class="preview-item__visual">
<img src="https://cdn.prod.website-files.com/6889f182607452ec007a0ae1/688a1e349d92acc75bd79fa8_Minimalist%20Green%20Stools.avif" class="preview-item__visual-img">
</div>
</a>
</div>
</div>
</div>
<div data-follower-cursor="" class="preview-follower">
<div data-follower-cursor-inner="" class="preview-follower__inner">
<div class="preview-follower__label">
<div class="preview-follower__label-span">View case</div>
</div>
</div>
</div>
</div>Step 2: Add CSS
CSS
.preview-container {
width: 100%;
max-width: 76em;
margin-left: auto;
margin-right: auto;
padding-left: 2em;
padding-right: 2em;
}
.preview-collection {
width: 100%;
margin-top: .5em;
}
.preview-item__row {
flex-flow: wrap;
justify-content: flex-start;
align-items: center;
width: 100%;
display: flex;
}
.preview-item__col {
flex: 1;
}
.preview-item__col.is--large {
max-width: 45%;
}
.preview-item__col.is--medium {
max-width: 25%;
}
.preview-item__col.is--small {
max-width: 15%;
}
.preview-container__label {
color: #0a0a0a80;
text-transform: uppercase;
font-size: .75em;
}
.preview-list {
flex-flow: column;
width: 100%;
display: flex;
position: relative;
}
.preview-item {
width: 100%;
transition: opacity .2s;
}
.preview-item__heading {
margin-top: 0;
margin-bottom: 0;
font-size: 3.5em;
font-weight: 400;
line-height: 1;
}
.preview-item__text {
margin-bottom: 0;
font-size: 1.25em;
font-weight: 400;
line-height: 1.2;
}
.preview-item__visual {
aspect-ratio: 1 / 1.25;
width: 20em;
display: none;
position: absolute;
overflow: hidden;
}
.preview-follower [data-follower-visual]{
display: block;
width: 100%;
height: 100%;
z-index: 0;
}
.preview-item__inner {
border-top: 1px solid #00000040;
width: 100%;
padding-top: 2.5em;
padding-bottom: 2.5em;
}
.preview-item__visual-img {
object-fit: cover;
width: 100%;
height: 100%;
}
.preview-follower {
z-index: 100;
aspect-ratio: 1 / 1.25;
pointer-events: none;
border-radius: .75em;
justify-content: center;
align-items: center;
width: 20em;
display: flex;
position: fixed;
inset: 0% auto auto 0%;
overflow: hidden;
}
.preview-follower__label {
z-index: 2;
position: absolute;
opacity: 0;
transform: translate(0px, 100%);
transition: opacity 0.1s ease, transform 0.6s cubic-bezier(0.65, 0.1, 0, 1);
}
.preview-follower__label-span {
background-color: #fff;
border-radius: .25em;
padding: .75em 1.25em;
font-size: 1em;
}
.preview-follower__inner {
z-index: 2;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
position: relative;
opacity: 0;
transform: scale(0);
transition: opacity 0.1s ease, transform 0.6s cubic-bezier(0.65, 0.1, 0, 1);
}
@media screen and (min-width: 992px){
.preview-item:last-of-type{
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}
}
@media (hover: hover) and (min-width: 992px){
body:has( [data-follower-collection]:hover) .preview-follower__inner{
opacity: 1;
transform: scale(1);
}
body:has( [data-follower-collection]:hover) .preview-follower__label{
opacity: 1;
transform: translate(0px, 0%);
}
body:has( .preview-item:hover) .preview-item:not(:hover){
opacity: 0.5;
}
}
@media screen and (max-width: 991px) {
.preview-item__row {
grid-row-gap: .5em;
}
.preview-item__row.tablet--hide {
display: none;
}
.preview-item__col.is--large {
flex: none;
order: -1;
width: 100%;
max-width: none;
}
.preview-item__col.is--medium {
order: -1;
max-width: 80%;
}
.preview-item__col.is--small {
text-align: right;
max-width: 20%;
}
.preview-item__col.is--small.tablet--hide {
display: none;
}
.preview-list {
grid-column-gap: 1em;
grid-row-gap: 4em;
flex-flow: wrap;
}
.preview-item {
width: calc(50% - .5em);
}
.preview-item__heading {
font-size: 2em;
}
.preview-item__visual {
border-radius: .75em;
order: -1;
width: 100%;
margin-bottom: 1em;
display: block;
position: relative;
}
.preview-item__inner {
border: 1px #000;
flex-flow: column;
padding-top: 0;
padding-bottom: 0;
display: flex;
}
.preview-follower {
display: none;
}
}
@media screen and (max-width: 767px) {
.preview-container {
padding-left: 1em;
padding-right: 1em;
}
.preview-list {
grid-row-gap: 3em;
}
.preview-item {
width: 100%;
}
}Step 2: Add Javascript
Step 3: Add Javascript
Javascript
function initPreviewFollower() {
// Find every follower wrap
const wrappers = document.querySelectorAll('[data-follower-wrap]');
wrappers.forEach(wrap => {
const collection = wrap.querySelector('[data-follower-collection]');
const items = wrap.querySelectorAll('[data-follower-item]');
const follower = wrap.querySelector('[data-follower-cursor]');
const followerInner = wrap.querySelector('[data-follower-cursor-inner]');
let prevIndex = null;
let firstEntry = true;
const offset = 100; // The animation distance in %
const duration = 0.5; // The animation duration of all visual transforms
const ease = 'power2.inOut';
// Initialize follower position
gsap.set(follower, { xPercent: -50, yPercent: -50 });
// Quick setters for x/y
const xTo = gsap.quickTo(follower, 'x', { duration: 0.6, ease: 'power3' });
const yTo = gsap.quickTo(follower, 'y', { duration: 0.6, ease: 'power3' });
// Move all followers on mousemove
window.addEventListener('mousemove', e => {
xTo(e.clientX);
yTo(e.clientY);
});
// Enter/leave per item within this wrap
items.forEach((item, index) => {
item.addEventListener('mouseenter', () => {
const forward = prevIndex === null || index > prevIndex;
prevIndex = index;
// animate out existing visuals
follower.querySelectorAll('[data-follower-visual]').forEach(el => {
gsap.killTweensOf(el);
gsap.to(el, {
yPercent: forward ? -offset : offset,
duration,
ease,
overwrite: 'auto',
onComplete: () => el.remove()
});
});
// clone & insert new visual
const visual = item.querySelector('[data-follower-visual]');
if (!visual) return;
const clone = visual.cloneNode(true);
followerInner.appendChild(clone);
// animate it in (unless it's the very first entry)
if (!firstEntry) {
gsap.fromTo(clone,
{ yPercent: forward ? offset : -offset },
{ yPercent: 0, duration, ease, overwrite: 'auto' }
);
} else {
firstEntry = false;
}
});
item.addEventListener('mouseleave', () => {
const el = follower.querySelector('[data-follower-visual]');
if (!el) return;
gsap.killTweensOf(el);
gsap.to(el, {
yPercent: -offset,
duration,
ease,
overwrite: 'auto',
onComplete: () => el.remove()
});
});
});
// If pointer leaves the collection, clear any visuals
collection.addEventListener('mouseleave', () => {
follower.querySelectorAll('[data-follower-visual]').forEach(el => {
gsap.killTweensOf(el);
gsap.delayedCall(duration, () => el.remove());
});
firstEntry = true;
prevIndex = null;
});
});
}
// Initialize Image Preview Cursor Follower
document.addEventListener("DOMContentLoaded", () =>{
initPreviewFollower();
})Implementation
Wrap
The [data-follower-wrap] attribute goes on an element that contains both the [data-follower-collection] and the [data-follower-cursor]. This allows you to have multiple wrap elements on a page, each with unique items and/or 'followers' inside.
Container
Inside each [data-follower-wrap], mark the parent of all items with [data-follower-collection]. The script listens for mouseleave on this container to know when to clear the cursor visuals.
Items
Every element you want to trigger a cursor-overlay on must have the [data-follower-item] attribute. The script:
- Listens for
mouseenteron each item - Clones its inner visual into the cursor
- Animates in/out based on the item’s index order
Visuals
Within each [data-follower-item], your visual (image, video, etc.) must have [data-follower-visual]. This is what gets cloned and animated into the cursor.
- On mouseenter, the script:
- Kills any existing tweens → animates old visuals out
- Clones the new
[data-follower-visual]→ appends it to[data-follower-cursor-inner] - Animates it in (direction based on hover order)
- On mouseleave (of either the item or the entire collection), it animates the visual out and removes it.
Cursor
Provide the two elements that form your custom cursor inside of the [data-follower-wrap]:
<div data-follower-cursor>
<div data-follower-cursor-inner>
<!-- cloned visuals will be appended here -->
</div>
</div>[data-follower-cursor]is the outer element that follows the pointer.[data-follower-cursor-inner]is the container where cloned visuals get injected.
Customizing the animation
In our example we do a fairly basic transform on the y-axis, inside of an overflow: hidden div to create a 'masking' effect. You can customize the GSAP animations in the mouseenter and mouseleave listeners however you want, to get the exact effect that you're after!
Resource details
Published
July 30, 2025
Category
Cursor Animations
Popularity
2.8K visitors
Need help?
Join Slack