# Staggered grid reveal in WordPress with GSAP

> Animate a grid of cards into view with a staggered scroll reveal in WordPress using GSAP and ScrollTrigger. Directional stagger, no plugin required.

Published 2026-06-10 by Jake. Canonical: https://gsapvault.com/blog/stagger-grid-reveal-wordpress

---
A staggered grid reveal animates a set of cards into view one after another instead of all at once. Portfolio grids, product listings, and team photos all read better when the items cascade in with a short delay between each. Here's how to build a basic version for WordPress using GSAP and ScrollTrigger, with no page builder add-on required. If you want to compare approaches first, [Animate on Scroll: 3 Approaches Compared](/blog/how-to-animate-on-scroll) covers the trade-offs, and [GSAP in WordPress](/gsap-wordpress) covers the setup side: where to enqueue the library, and what breaks it once the site is live.

## What you'll build

A responsive grid where each item fades and slides up as the grid scrolls into view. The items animate in sequence using GSAP's grid-aware stagger, so the wave follows the layout rather than raw document order. This basic version reveals once per scroll, adapts to any column count, and respects reduced motion preferences.

<div id="grid-demo" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1.5rem 0 2rem;">
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">1</div>
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">2</div>
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">3</div>
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">4</div>
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">5</div>
  <div class="grid-demo-item" style="aspect-ratio: 4/3; border-radius: 12px; background: rgba(200, 255, 0, 0.08); border: 1px solid rgba(200, 255, 0, 0.2); display: flex; align-items: center; justify-content: center; font-family: 'Syne', sans-serif; font-weight: 700; font-size: 1.5rem; color: var(--lime, #c8ff00);">6</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  if (typeof gsap === 'undefined') return;

  var items = document.querySelectorAll('#grid-demo .grid-demo-item');
  if (!items.length) return;

  gsap.set(items, { opacity: 0, y: 40 });

  function play() {
    gsap.to(items, {
      opacity: 1,
      y: 0,
      duration: 0.7,
      ease: 'power3.out',
      stagger: { each: 0.08, grid: 'auto', from: 'start' }
    });
  }

  if (window.ScrollTrigger) {
    gsap.registerPlugin(ScrollTrigger);
    ScrollTrigger.create({ trigger: '#grid-demo', start: 'top 85%', once: true, onEnter: play });
  } else {
    play();
  }
});
</script>

## Why GSAP for this

You can fake a stagger in CSS by adding an increasing `transition-delay` to each item, but that approach has limits:

- **You hardcode each delay**, so adding or removing cards means editing the CSS by hand.
- **CSS delays follow document order**, not the visual grid, so a diagonal or center-out wave is painful to build.
- **There's no clean scroll trigger**; you end up bolting on an Intersection Observer anyway.

GSAP's [`stagger`](https://gsap.com/resources/getting-started/Staggers/) accepts a `grid` option that indexes items by row and column, so the wave follows the layout automatically. Pair it with ScrollTrigger and you get a reveal that fires at the right scroll position with a couple of lines of setup. For the wider CSS versus JavaScript picture, see [GSAP vs CSS Animations: A Practical Guide](/blog/gsap-vs-css-animations).

## Step 1: Add GSAP

GSAP loads from a CDN, so there's nothing to install. Use a plugin like WPCode or "Insert Headers and Footers" to add these two scripts to your site's footer:

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
```

The version is pinned to `3.14.2` so a future GSAP release can't change behavior without you choosing to update.

## Step 2: Add the HTML

Add this markup wherever you want the grid. In Gutenberg, use a Custom HTML block. In Elementor or Bricks, use an HTML widget. The `data-reveal-grid` attribute marks the container and `data-reveal-item` marks each animated child.

```html
<div class="reveal-grid" data-reveal-grid>
  <div class="reveal-card" data-reveal-item>Card 1</div>
  <div class="reveal-card" data-reveal-item>Card 2</div>
  <div class="reveal-card" data-reveal-item>Card 3</div>
  <div class="reveal-card" data-reveal-item>Card 4</div>
  <div class="reveal-card" data-reveal-item>Card 5</div>
  <div class="reveal-card" data-reveal-item>Card 6</div>
</div>
```

The cards can hold anything: images, headings, links, or full project cards. The script only cares about the two data attributes.

## Step 3: Add the CSS

Add this via your theme's custom CSS field, the WordPress Customizer, or WPCode as a CSS snippet:

```css
.reveal-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.reveal-card {
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: #1a1a1a;
  color: #fff;
  will-change: transform, opacity;
}

@media (max-width: 700px) {
  .reveal-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
```

A couple of notes: `grid-template-columns` sets the column count, which the stagger reads at runtime, so you don't repeat the number anywhere in JavaScript. `will-change: transform, opacity` hints to the browser that these properties will animate, which keeps the reveal on the GPU.

## Step 4: Add the JavaScript

Add this script after the two GSAP tags:

```javascript
document.addEventListener('DOMContentLoaded', function() {
  // Respect reduced motion preferences
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  gsap.registerPlugin(ScrollTrigger);

  document.querySelectorAll('[data-reveal-grid]').forEach(function(grid) {
    var items = grid.querySelectorAll('[data-reveal-item]');
    if (!items.length) return;

    // Hidden starting state
    gsap.set(items, { opacity: 0, y: 40 });

    ScrollTrigger.create({
      trigger: grid,
      start: 'top 80%',
      once: true,
      onEnter: function() {
        gsap.to(items, {
          opacity: 1,
          y: 0,
          duration: 0.8,
          ease: 'power3.out',
          stagger: {
            each: 0.08,   // seconds between each item
            grid: 'auto', // read rows and columns from the layout
            from: 'start' // begin at the top-left item
          }
        });
      }
    });
  });
});
```

Here's what each part does:

- **`gsap.set(items, { opacity: 0, y: 40 })`** hides the cards and nudges them down before the reveal fires. Setting this in JavaScript rather than CSS avoids a flash of hidden content if the script fails to load; the cards stay visible in that case.
- **`ScrollTrigger.create({ start: 'top 80%', once: true })`** fires the reveal when the top of the grid reaches 80 percent down the viewport, and `once` stops it re-triggering on scroll back up.
- **`stagger: { grid: 'auto' }`** tells GSAP to infer the grid shape from the DOM so the delay follows rows and columns. Combined with `from: 'start'`, the wave flows from the top-left corner.

### Change the wave direction

The `from` value controls where the stagger originates. Swap it for a different feel:

```javascript
stagger: {
  each: 0.08,
  grid: 'auto',
  from: 'center' // ripple outward from the middle
}
```

`from` accepts `'start'`, `'end'`, `'center'`, `'edges'`, `'random'`, or an item index. `'center'` gives an explosion-from-the-middle look, and `'end'` reverses the flow to start at the bottom-right.

## Step 5: Add it to WordPress

You have three pieces: the two GSAP tags (Step 1), the CSS (Step 3), and the JavaScript (Step 4). Here's how to place them.

### Option A: WPCode plugin (recommended)

**Snippet 1: CSS (site-wide).** Go to Code Snippets > Add Snippet > "Add Your Custom Code". Set the code type to "CSS Snippet", paste the CSS from Step 3, toggle on, and save.

**Snippet 2: Footer scripts (site-wide).** Create another snippet, set the code type to "HTML Snippet", and paste both GSAP tags followed by the effect script wrapped in a `<script>` tag:

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  gsap.registerPlugin(ScrollTrigger);

  document.querySelectorAll('[data-reveal-grid]').forEach(function(grid) {
    var items = grid.querySelectorAll('[data-reveal-item]');
    if (!items.length) return;

    gsap.set(items, { opacity: 0, y: 40 });

    ScrollTrigger.create({
      trigger: grid,
      start: 'top 80%',
      once: true,
      onEnter: function() {
        gsap.to(items, {
          opacity: 1,
          y: 0,
          duration: 0.8,
          ease: 'power3.out',
          stagger: { each: 0.08, grid: 'auto', from: 'start' }
        });
      }
    });
  });
});
</script>
```

Set the insertion location to "Site Wide Footer", toggle on, and save.

**Snippet 3: Grid HTML (per page).** Add the grid markup from Step 2 wherever you need it using a Custom HTML block. The script scans for every `[data-reveal-grid]` on the page, so you can drop multiple grids on one page and each animates independently.

### Option B: Child theme

Add the CSS to your child theme's `style.css`. Then enqueue GSAP, ScrollTrigger, and your script through `functions.php`:

```php
add_action('wp_enqueue_scripts', function() {
  wp_enqueue_script('gsap', 'https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js', [], '3.14.2', true);
  wp_enqueue_script('scrolltrigger', 'https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js', ['gsap'], '3.14.2', true);
  wp_enqueue_script('grid-reveal', get_stylesheet_directory_uri() . '/js/grid-reveal.js', ['gsap', 'scrolltrigger'], '1.0', true);
});
```

Save the JavaScript from Step 4 as `js/grid-reveal.js` in your child theme folder. The dependency array keeps the load order correct: GSAP first, then ScrollTrigger, then your script.

## Common issues

**The whole grid animates before you scroll to it.** ScrollTrigger measures position on load. If your grid sits above the fold, it fires immediately, which is expected. For content further down that still triggers too early, tighten the `start` value from `top 80%` to `top 60%`.

**Cards flash visible then hide.** This happens when the CSS renders before `gsap.set` runs. Keep the hidden state in JavaScript as shown so the cards only hide once GSAP is confirmed loaded. Do not also hide them with `opacity: 0` in CSS, or reduced-motion users will see nothing.

**The stagger order looks wrong.** `grid: 'auto'` reads the rendered grid, so it needs the items laid out as a real CSS grid. If your cards use flexbox or floats instead, pass an explicit shape like `grid: [2, 3]` for two rows and three columns.

**Nothing animates after a layout change.** If you toggle columns with JavaScript or load cards via AJAX, call `ScrollTrigger.refresh()` afterward so positions are remeasured.

**A caching plugin serves the old page.** Clear your cache (WP Rocket, W3 Total Cache, and similar) after adding or editing the snippets.

## That's it

The grid reveals with a staggered wave as it scrolls into view, adapts to your column count, and fires once. Add more `data-reveal-grid` containers on the same page and each runs on its own. Visitors who prefer reduced motion see the full grid immediately with no animation.

To customize: raise `each` for a slower cascade, change `from` for a different wave origin, or swap the `y: 40` starting offset for `scale: 0.8` to get a pop-in instead of a slide.

---

## Want more control?

The [full Stagger Grid Reveal effect](/effects/stagger-grid-reveal) includes features this basic version doesn't have:

- **Eight stagger directions** including diagonal and center-outward waves, plus `edges` and `random` patterns
- **Seven animation styles** covering fade, scale, flip, slide, blur, rotate, and bounce, each with a tuned from-state
- **Wave and spiral stagger patterns** for organic motion across larger grids
- **ScrollTrigger batch processing** that groups items entering the viewport together for smoother performance on long pages
- **Interactive hover tilt** with 3D perspective on pointer devices
- **Configurable grid columns, gap, and item dimensions** from data attributes alone
- **Responsive breakpoint handling** with automatic recalculation when the layout changes
- **Lenis smooth scroll compatibility** for pages that use smooth scrolling

[View the full effect with all options →](/effects/stagger-grid-reveal)

---

## Related articles

- **[GSAP in WordPress: the complete guide](/gsap-wordpress)** - Where to enqueue GSAP, block editor and page builder realities, and every WordPress tutorial on this site in one place.
- **[Animate on Scroll: 3 Approaches Compared](/blog/how-to-animate-on-scroll)** - Compare CSS scroll animations, Intersection Observer, and GSAP ScrollTrigger for reveal effects.
- **[Adding an Infinite Marquee to WordPress](/blog/infinite-marquee-wordpress)** - Another step-by-step WordPress tutorial using GSAP, this time for scrolling strips.
- **[GSAP vs CSS Animations: A Practical Guide](/blog/gsap-vs-css-animations)** - When CSS transitions are enough and when you need JavaScript animation.
- **[Adding a Text Scramble Effect to WordPress](/blog/text-scramble-wordpress)** - A scroll-triggered text reveal you can drop into any WordPress page.