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/

Elements Reveal on Scroll

Elements Reveal on Scroll

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>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.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

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy

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
gsap.registerPlugin(ScrollTrigger);

function initContentRevealScroll(){
  const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const ctx = gsap.context(() => {

    document.querySelectorAll('[data-reveal-group]').forEach(groupEl => {
      // Config from attributes or defaults (group-level)
      const groupStaggerSec = (parseFloat(groupEl.getAttribute('data-stagger')) || 100) / 1000; // ms → sec
      const groupDistance = groupEl.getAttribute('data-distance') || '2em';
      const triggerStart = groupEl.getAttribute('data-start') || 'top 80%';

      const animDuration = 0.8;
      const animEase = "power4.inOut";

      // Reduced motion: show immediately
      if (prefersReduced) {
        gsap.set(groupEl, { clearProps: 'all', y: 0, autoAlpha: 1 });
        return;
      }

      // If no direct children, animate the group element itself
      const directChildren = Array.from(groupEl.children).filter(el => el.nodeType === 1);
      if (!directChildren.length) {
        gsap.set(groupEl, { y: groupDistance, autoAlpha: 0 });
        ScrollTrigger.create({
          trigger: groupEl,
          start: triggerStart,
          once: true,
          onEnter: () => gsap.to(groupEl, { 
            y: 0, 
            autoAlpha: 1, 
            duration: animDuration, 
            ease: animEase,
            onComplete: () => gsap.set(groupEl, { clearProps: 'all' })
          })
        });
        return;
      }

      // Build animation slots: item or nested (deep layers allowed)
      const slots = [];
      directChildren.forEach(child => {
        const nestedGroup = child.matches('[data-reveal-group-nested]')
          ? child
          : child.querySelector(':scope [data-reveal-group-nested]');

        if (nestedGroup) {
          const includeParent = child.getAttribute('data-ignore') === 'false' || nestedGroup.getAttribute('data-ignore') === 'false';
          slots.push({ type: 'nested', parentEl: child, nestedEl: nestedGroup, includeParent });
        } else {
          slots.push({ type: 'item', el: child });
        }
      });

      // Initial hidden state
      slots.forEach(slot => {
        if (slot.type === 'item') {
          // If the element itself is a nested group, force group distance (prevents it from using its own data-distance)
          const isNestedSelf = slot.el.matches('[data-reveal-group-nested]');
          const d = isNestedSelf ? groupDistance : (slot.el.getAttribute('data-distance') || groupDistance);
          gsap.set(slot.el, { y: d, autoAlpha: 0 });
        } else {
          // Parent follows the group's distance when included, regardless of nested's data-distance
          if (slot.includeParent) gsap.set(slot.parentEl, { y: groupDistance, autoAlpha: 0 });
          // Children use nested group's own distance (fallback to group distance)
          const nestedD = slot.nestedEl.getAttribute('data-distance') || groupDistance;
          Array.from(slot.nestedEl.children).forEach(target => gsap.set(target, { y: nestedD, autoAlpha: 0 }));
        }
      });

      // Extra safety: if a nested parent is included, re-assert its distance to the group's value
      slots.forEach(slot => {
        if (slot.type === 'nested' && slot.includeParent) {
          gsap.set(slot.parentEl, { y: groupDistance }); 
        }
      });

      // Reveal sequence
      ScrollTrigger.create({
        trigger: groupEl,
        start: triggerStart,
        once: true,
        onEnter: () => {
          const tl = gsap.timeline();

          slots.forEach((slot, slotIndex) => {
            const slotTime = slotIndex * groupStaggerSec;

            if (slot.type === 'item') {
              tl.to(slot.el, { 
                y: 0, 
                autoAlpha: 1, 
                duration: animDuration, 
                ease: animEase,
                onComplete: () => gsap.set(slot.el, { clearProps: 'all' })
              }, slotTime);
            } else {
              // Optionally include the parent at the same slot time (parent uses group distance)
              if (slot.includeParent) {
                tl.to(slot.parentEl, {
                  y: 0,
                  autoAlpha: 1,
                  duration: animDuration,
                  ease: animEase,
                  onComplete: () => gsap.set(slot.parentEl, { clearProps: 'all' })
                }, slotTime);
              }
              // Nested children use nested stagger (ms → sec); fallback to group stagger
              const nestedMs = parseFloat(slot.nestedEl.getAttribute('data-stagger'));
              const nestedStaggerSec = isNaN(nestedMs) ? groupStaggerSec : nestedMs / 1000;
              Array.from(slot.nestedEl.children).forEach((nestedChild, nestedIndex) => {
                tl.to(nestedChild, { 
                  y: 0, 
                  autoAlpha: 1, 
                  duration: animDuration, 
                  ease: animEase,
                  onComplete: () => gsap.set(nestedChild, { clearProps: 'all' })
                }, slotTime + nestedIndex * nestedStaggerSec);
              });
            }
          });
        }
      });
    });

  });

  return () => ctx.revert();
}

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

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

Implementation

This setup gives you a flexible way to reveal content blocks with GSAP + ScrollTrigger, including support for nested groups with independent staggers and distances. You don’t need to set every attribute on each group because defaults are provided for everything. Only add attributes when you need to override the defaults.

1. Wrap your reveal items inside a container with the data-reveal-group attribute. This will animate all of the direct children elements inside this wrapper:

<div data-reveal-group>
  <!-- children to reveal -->
</div>

2. Inside your data-reveal-group, you can add any HTML structure.

  • Direct children will animate in sequence based on the group’s data-stagger.
  • If a direct child contains data-reveal-group-nested, that nested group’s children will reveal in their own sequence.
  • By default, the parent of a nested group is skipped from the main reveal. Add data-ignore="false" to the parent (or nested group) if you also want it included in the main group’s animation.

3. Here's a quick example structure with comments to explain what happens:

<div data-reveal-group>
  <img> <!-- this item will reveal -->
  <img> <!-- this item will reveal -->
  <div data-reveal-group-nested> <!-- this item will NOT reveal -->
    <img> <!-- this item will reveal -->
    <img> <!-- this item will reveal -->
    <img> <!-- this item will reveal -->
  </div> 
  <div data-reveal-group-nested data-ignore="false"> <!-- this item WILL reveal -->
    <img> <!-- this item will reveal -->
    <img> <!-- this item will reveal -->
    <img> <!-- this item will reveal -->
  </div>   
</div>

Available attributes

Group

Use [data-reveal-group] on a wrapper to animate all direct children one-by-one (default) unless a child contains a nested group; required for each group.

Nested

Place [data-reveal-group-nested] on a child so its own children animate in sequence when the parent’s turn comes, skipping the parent by default unless data-ignore=“false” is set.

Stagger

Set [data-stagger] (default 100 ms) to control the delay between animations for direct children or nested children, depending on where it’s applied.

Distance

Use [data-distance] (default 2em) to define the starting offset for animations, applying to all children of a group or only to nested children when set on a nested group.

Start

Set [data-start] (default top 80%) to define the ScrollTrigger start position for when the group’s reveal begins.

Ignore

Add [data-ignore="false"] to include a nested group’s parent in the main reveal sequence while still animating its nested children.

Resource Details

Scrolling
Scrolltrigger
GSAP
Stagger
Reveal
Animation
Setup

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.