Copied SVG to clipboard
Something went wrong
Copied code to clipboard
Something went wrong
Saved to bookmarks!
Removed from bookmarks
Webflow Challenge: Win $5K

Default

User image

Default

Name

  • -€50
    Upgrade to Lifetime
The Vault/

Image Preview Cursor Follower

Image Preview Cursor Follower

Documentation

Webflow

Code

Setup: External Scripts

External Scripts in Webflow

Make sure to always put the External Scripts before the Javascript step of the resource.

In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?

HTML

Copy
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>

Step 1: Copy structure to Webflow

Copy structure to Webflow

In the video below we described how you can copy + paste the structure of this resource to your Webflow project.

Copy to Webflow

Webflow structure is not required for this resource.

Step 1: Add HTML

HTML

Copy
<div data-follower-wrap="" class="preview-container">
  <div class="preview-item__row tabelt--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>

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy
.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.tabelt--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 custom Javascript

Custom Javascript in Webflow

In this video, Ilja gives you some guidance about using JavaScript in Webflow:

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
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 easing = '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;
    });
  });
}

document.addEventListener("DOMContentLoaded", () =>{
  initPreviewFollower();
})

Step 3: Add custom CSS

Step 2: Add custom CSS

Custom CSS in Webflow

Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:

CSS

Copy
.preview-follower__inner,
.preview-follower__label{
  transition: opacity 0.1s ease, transform 0.6s cubic-bezier(0.65, 0.1, 0, 1);
}

/* html:not(.wf-design-mode) */ .preview-follower__inner{
  opacity: 0;
  transform: scale(0);
}

/* html:not(.wf-design-mode) */ .preview-follower__label{
  opacity: 0;
  transform: translate(0px, 100%);
}

.preview-follower [data-follower-visual]{
  display: block;
  width: 100%;
  height: 100%;
  z-index: 0;
}

@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;
  }
}

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:

  1. Listens for mouseenter on each item
  2. Clones its inner visual into the cursor
  3. 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:
    1. Kills any existing tweens → animates old visuals out
    2. Clones the new data-follower-visual → appends it to data-follower-cursor-inner
    3. 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

Element
Follow
Cursor
Image
GSAP
Directional
Interaction
List

Original source

Ilja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.