Elastic Grid Scroll: Creating Lag-Based Layout Animations with GSAP ScrollSmoother

A scroll effect where each column of a grid moves at a slightly different speed, creating a soft, elastic feel as you scroll.

You’ve probably seen this kind of scroll effect before, even if it doesn’t have a name yet. (Honestly, we need a dictionary for all these weird and wonderful web interactions. If you’ve got a talent for naming things…do it. Seriously. The internet is waiting.)

Imagine a grid of images. As you scroll, the columns don’t move uniformly but instead, the center columns react faster, while those on the edges trail behind slightly. It feels soft, elastic, and physical, almost like scrolling with weight, or elasticity.

You can see this amazing effect on sites like yzavoku.com (and I’m sure there’s a lot more!).

So what better excuse to use the now-free GSAP ScrollSmoother? We can recreate it easily, with great performance and full control. Let’s have a look!

What We’re Building

We’ll take CSS grid based layout and add some magic:

  • Inertia-based scrolling using ScrollSmoother
  • Per-column lag, calculated dynamically based on distance from the center
  • A layout that adapts to column changes

HTML Structure

Let’s set up the markup with figures in a grid:

<div class="grid">
  <figure class="grid__item">
    <div class="grid__item-img" style="background-image: url(assets/1.webp)"></div>
    <figcaption class="grid__item-caption">Zorith - L91</figcaption>
  </figure>
  <!-- Repeat for more items -->
</div>

Inside the grid, we have many .grid__item figures, each with a background image and a label. These will be dynamically grouped into columns by JavaScript, based on how many columns CSS defines.

CSS Grid Setup

.grid {
  display: grid;
  grid-template-columns: repeat(var(--column-count), minmax(var(--column-size), 1fr));
  grid-column-gap: var(--c-gap);
  grid-row-gap: var(--r-gap);
}

.grid__column {
  display: flex;
  flex-direction: column;
  gap: var(--c-gap);
}

We define all the variables in our root.

In our JavaScript then, we’ll change the DOM structure by inserting .grid__column wrappers around groups of items, one per colum, so we can control their motion individually. Why are we doing this? It’s a bit lighter to move columns rather then each individual item.

JavaScript + GSAP ScrollSmoother

Let’s walk through the logic step-by-step.

1. Enable Smooth Scrolling and Lag Effects

gsap.registerPlugin(ScrollTrigger, ScrollSmoother);

const smoother = ScrollSmoother.create({
  smooth: 1, // Inertia intensity
  effects: true, // Enable per-element scroll lag
  normalizeScroll: true, // Fixes mobile inconsistencies
});

This activates GSAP’s smooth scroll layer. The effects: true flag lets us animate elements with lag, no scroll listeners needed.

2. Group Items Into Columns Based on CSS

const groupItemsByColumn = () => {
  const gridStyles = window.getComputedStyle(grid);
  const columnsRaw = gridStyles.getPropertyValue('grid-template-columns');

  const numColumns = columnsRaw.split(' ').filter(Boolean).length;

  const columns = Array.from({ length: numColumns }, () => []); // Initialize column arrays

  // Distribute grid items into column buckets
  grid.querySelectorAll('.grid__item').forEach((item, index) => {
    columns[index % numColumns].push(item);
  });

  return { columns, numColumns };
};

This method groups your grid items into arrays, one for each visual column, using the actual number of columns calculated from the CSS.

3. Create Column Wrappers and Assign Lag

const buildGrid = (columns, numColumns) => {

  const fragment = document.createDocumentFragment(); // Efficient DOM batch insertion
  const mid = (numColumns - 1) / 2; // Center index (can be fractional)
  const columnContainers = [];

  // Loop over each column
  columns.forEach((column, i) => {
    const distance = Math.abs(i - mid); // Distance from center column
    const lag = baseLag + distance * lagScale; // Lag based on distance from center

    const columnContainer = document.createElement('div'); // New column wrapper
    columnContainer.className = 'grid__column';

    // Append items to column container
    column.forEach((item) => columnContainer.appendChild(item));

    fragment.appendChild(columnContainer); // Add to fragment
    columnContainers.push({ element: columnContainer, lag }); // Save for lag effect setup
  });

  grid.appendChild(fragment); // Add all columns to DOM at once
  return columnContainers;
};

The lag value increases the further a column is from the center, creating that elastic “catch up” feel during scroll.

4. Apply Lag Effects to Each Column

const applyLagEffects = (columnContainers) => {
  columnContainers.forEach(({ element, lag }) => {
    smoother.effects(element, { speed: 1, lag }); // Apply individual lag per column
  });
};

ScrollSmoother handles all the heavy lifting, we just pass the desired lag.

5. Handle Layout on Resize

// Rebuild the layout only if the number of columns has changed on window resize
window.addEventListener('resize', () => {
  const newColumnCount = getColumnCount();
  if (newColumnCount !== currentColumnCount) {
    init();
  }
});

This ensures our layout stays correct across breakpoints and column count changes (handled via CSS).

And that’s it!

Variation 4

Extend This Further

Now, there’s lots of ways to build upon this and add more jazz!

For example, you could:

  • add scroll-triggered opacity or scale animations
  • use scroll velocity to control effects (see demo 2)
  • adapt this pattern for horizontal scroll layouts

Exploring Variations

Once you have the core concept in place, there are four demo variations you can explore. Each one shows how different lag values and scroll-based interactions can influence the experience.

You can adjust which columns respond faster, or play with subtle scaling and transforms based on scroll velocity. Even small changes can shift the rhythm and tone of the layout in interesting ways. And don’t forget: changing the look of the grid itself, like the image ratio or gaps, will give this a whole different feel!

Now it’s your turn. Tweak it, break it, rebuild it, and make something cool.

I really hope you enjoy this effect! Thanks for checking by 🙂

Manoela Ilic

Editor-in-Chief at Codrops. Designer, developer, and dreamer — sharing web inspiration with millions since 2009. Bringing together 20+ years of code, creativity, and community.

The
New
Collective

🎨✨💻 Stay ahead of the curve with handpicked, high-quality frontend development and design news, picked freshly every single day. No fluff, no filler—just the most relevant insights, inspiring reads, and updates to keep you in the know.

Prefer a weekly digest in your inbox? No problem, we got you covered. Just subscribe here.