GSAP Stagger: Animate Lists and Grids with Rhythm (2026)
Learn how to use GSAP stagger to animate multiple elements with perfect timing. Covers basic stagger, advanced object syntax, from options, and real-world grid reveals.

When I animate a list of cards without stagger, it looks wrong. All six cards appear at the same time, like a wall just dropped. The moment I add a stagger, the same animation feels intentional. Each card enters with a slight offset and the whole thing reads as a designed sequence.
Stagger is one of the most impactful things you can add to any list or grid animation. GSAP makes it extremely flexible.
The Basics
The simplest form: a number representing the seconds between each element's animation start.
gsap.from('.card', {
y: 30,
opacity: 0,
duration: 0.6,
ease: 'power3.out',
stagger: 0.1, // each card starts 0.1s after the previous
})
GSAP targets all .card elements and offsets each one by 0.1 seconds. The first card starts immediately. The second starts at 0.1s. The third at 0.2s. And so on.
This single property transforms a flat entrance into a flowing reveal.
Visual: PLACEHOLDER - Side-by-side of 6 cards appearing without stagger vs with stagger: 0.1
Stagger with a Timeline
Inside a timeline, stagger works exactly the same way. The staggered sequence becomes one step in a larger choreography:
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.grid',
start: 'top 80%',
once: true,
},
})
tl.from('.section-title', { y: 20, opacity: 0, duration: 0.7 })
tl.from(
'.card',
{
y: 30,
opacity: 0,
duration: 0.6,
ease: 'power3.out',
stagger: 0.1,
},
'-=0.3'
)
The title animates first. Then the cards cascade in, overlapping the title by 0.3 seconds. The whole section feels like one connected motion.
The Stagger Object
For more control, pass an object instead of a number.
amount vs each
There are two ways to define stagger timing:
// each: fixed offset between items (0.1s per item regardless of count)
stagger: {
each: 0.1
}
// amount: total stagger duration distributed across all items
stagger: {
amount: 0.6
} // 6 items = 0.1s each; 12 items = 0.05s each
I use each when I want consistent rhythm regardless of item count. I use amount when I want the total reveal to always take the same amount of time.
from
The from option controls which element starts first:
stagger: { each: 0.08, from: "start" } // left to right (default)
stagger: { each: 0.08, from: "end" } // right to left
stagger: { each: 0.08, from: "center" } // outward from center
stagger: { each: 0.08, from: "edges" } // inward from edges to center
stagger: { each: 0.08, from: "random" } // randomized order
Random stagger is particularly effective for organic-looking reveals:
gsap.from('.card', {
y: 20,
opacity: 0,
duration: 0.5,
stagger: {
each: 0.07,
from: 'random',
},
})
Elements animate in a scattered order instead of a predictable sequence. Good for grids where the diagonal left-to-right pattern would feel too mechanical.
Visual: PLACEHOLDER - Grid of cards showing 5 different stagger directions: start, end, center, edges, random
Grid-Aware Stagger
For two-dimensional grids, GSAP can calculate stagger based on grid position rather than DOM order. Pass the grid dimensions and GSAP treats the elements as rows and columns:
gsap.from('.card', {
y: 30,
opacity: 0,
duration: 0.6,
stagger: {
each: 0.05,
from: 'center',
grid: [3, 4], // 3 rows, 4 columns
},
})
With from: "center" and grid: [3, 4], elements radiate outward from the grid's center point. The DOM order doesn't matter. The animation reads as a 2D ripple.
You can also use grid: "auto" and GSAP will figure out the grid dimensions automatically:
stagger: {
each: 0.05,
from: "start",
grid: "auto"
}
This works when your grid items are laid out with CSS Grid or Flexbox and the columns are consistent. GSAP reads the rendered positions.
Function-Based Stagger
For completely custom timing, pass a function. GSAP calls it once per element with the index, element, and full targets array:
gsap.from('.item', {
y: 20,
opacity: 0,
duration: 0.5,
stagger: (index) => index * 0.08 + Math.random() * 0.05,
})
This produces a base stagger of 0.08s per item with a small random offset added. The result is natural without being chaotic.
A more practical use: different delays based on element type:
gsap.from('.cell', {
opacity: 0,
scale: 0.9,
duration: 0.4,
stagger: (index, target) => {
// Featured cells animate first
return target.classList.contains('featured') ? 0 : index * 0.06 + 0.3
},
})
Practical Examples
Feature Card Grid
A typical use case: animating a row of feature cards when they enter the viewport.
function animateFeatureCards() {
const cards = document.querySelectorAll('.feature-card')
gsap.from(cards, {
y: 40,
opacity: 0,
duration: 0.7,
ease: 'power3.out',
stagger: {
each: 0.1,
from: 'start',
},
scrollTrigger: {
trigger: '.feature-grid',
start: 'top 75%',
once: true,
},
})
}
Navigation Links
Stagger works well for menu items that animate in on page load or menu open:
function animateNavLinks() {
gsap.from('.nav-link', {
y: -10,
opacity: 0,
duration: 0.4,
ease: 'power2.out',
stagger: 0.06,
})
}
The offset is small (0.06s) because navigation items are close together. Too much stagger on short distances looks slow and disconnected.
List Items with Scroll Trigger
Long lists that reveal as you scroll:
gsap.from('.list-item', {
x: -20,
opacity: 0,
duration: 0.5,
ease: 'power2.out',
stagger: {
amount: 0.8, // total 0.8s for all items however many there are
from: 'start',
},
scrollTrigger: {
trigger: '.list',
start: 'top 80%',
once: true,
},
})
Using amount here means the total stagger is always 0.8 seconds, whether the list has 5 or 20 items.
Stagger with Repeat and Yoyo
Stagger also works on repeated animations. Each element loops with its own offset:
gsap.to('.dot', {
y: -15,
duration: 0.4,
ease: 'power2.out',
repeat: -1,
yoyo: true,
stagger: {
each: 0.1,
from: 'start',
repeat: -1,
},
})
This creates a bouncing dots loader where each dot animates in sequence continuously. The stagger.repeat: -1 keeps the stagger pattern looping rather than the dots gradually syncing up.
Performance Note
When animating many elements with stagger, animate opacity and transform properties (x, y, scale, rotation). These run on the compositor and don't trigger layout. Avoid animating width, height, top, left, or margin in stagger animations with many targets.
For very long lists (50+ items), consider animating only elements that are currently visible instead of staggering the entire list at once.
Common Mistakes
Stagger that's too large. A stagger of 0.5s on a 10-item grid means the last card doesn't animate for 4.5 seconds. Keep stagger values small: 0.05 to 0.15 seconds works for most use cases.
Forgetting once: true on scroll triggers. Without it, the stagger animation replays every time the user scrolls back and forth past the trigger point.
Using stagger on a single element. It's just a delay at that point. Use delay instead.
Animating layout properties. width, height, top, left with stagger on many elements is a guaranteed performance problem. Stick to transforms.
Key Takeaways
stagger: 0.1offsets each element by 0.1 seconds. Simple and effective.- The object syntax gives you
each,amount,from, andgridcontrols. from: "random"creates organic-looking reveals for grids.grid: "auto"orgrid: [rows, cols]enables 2D ripple patterns.- Function-based stagger gives you complete custom timing per element.
- Keep stagger values small (0.05 to 0.15s) and animate transforms only.
Take It Further
Stagger becomes even more powerful inside timelines. The GSAP Timeline Tutorial covers how to sequence staggered groups as part of larger choreography.
For scroll-triggered stagger, GSAP ScrollTrigger Examples has working examples with batch triggers for improved performance on long pages.
Browse the Annnimate library for production-ready animations that use these stagger patterns. Every animation includes the full GSAP code.