Elements Reveal on Scroll
Documentation
Webflow
Code
Setup: External Scripts
HTML
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/ScrollTrigger.min.js"></script>Step 1: Add HTML
HTML structure is not required for this resource.
Step 2: Add Javascript
Step 3: Add Javascript
Javascript
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') !== 'true' &&
(
child.getAttribute('data-ignore') === 'false' ||
nestedGroup.getAttribute('data-ignore') === 'false'
);
const nestedChildren = Array.from(nestedGroup.children).filter(
el => el.nodeType === 1 && el.getAttribute('data-ignore') !== 'true'
);
slots.push({
type: 'nested',
parentEl: child,
nestedEl: nestedGroup,
includeParent,
nestedChildren
});
} else {
if (child.getAttribute('data-ignore') === 'true') return;
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;
slot.nestedChildren.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;
slot.nestedChildren.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();
}
// Initialize Elements Reveal on Scroll
document.addEventListener("DOMContentLoaded", () => {
initContentRevealScroll();
});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.
Skip Element
Use [data-ignore="true"] on any element to exclude it from the animation. This works on direct children of a group as well as children inside a nested group, without affecting the rest of the sequence.
Include Parent
Add [data-ignore="false"] to a nested group or its parent to include the parent element in the main reveal sequence alongside its nested children.
Resource details
Published
August 13, 2025
Category
Scroll Animations
Popularity
4.6K visitors
Need help?
Join Slack