webdevpuneet.com

Scroll Grid Zoom — Free GSAP Tile Expand Snippet

Scroll Grid Zoom · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="sgz-top"><p>Scroll ↓</p></section>
<section class="sgz-stage" id="sgzStage">
  <div class="sgz-grid" id="sgzGrid">
    <div class="sgz-tile" style="--tc1:#312e81;--tc2:#1e1b4b"><span>🌊</span></div>
    <div class="sgz-tile" style="--tc1:#5b21b6;--tc2:#312e81"><span>🌌</span></div>
    <div class="sgz-tile" style="--tc1:#134e4a;--tc2:#0f2f2c"><span>🌿</span></div>
    <div class="sgz-tile" style="--tc1:#7c2d12;--tc2:#431407"><span>🏜️</span></div>
    <div class="sgz-tile sgz-center" id="sgzCenter" style="--tc1:#6366f1;--tc2:#0ea5e9">
      <span>🗻</span>
      <div class="sgz-center-copy" id="sgzCopy"><h2>The main event</h2><p>The middle tile grows to own the viewport.</p></div>
    </div>
    <div class="sgz-tile" style="--tc1:#831843;--tc2:#4c0519"><span>🌸</span></div>
    <div class="sgz-tile" style="--tc1:#1e3a8a;--tc2:#172554"><span>🧊</span></div>
    <div class="sgz-tile" style="--tc1:#3f3f46;--tc2:#18181b"><span>⛰️</span></div>
    <div class="sgz-tile" style="--tc1:#14532d;--tc2:#052e16"><span>🌲</span></div>
  </div>
</section>
<section class="sgz-bottom"><p>Scroll up and the tile shrinks back into its grid cell.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Index-derived scatter
Each tile exits along its own grid direction.
Measured zoom
Scale factor computed from rendered size.
Compositor scaling
transform: scale avoids nine-tile reflow.
Single camera move
Zoom and exits share timeline position 0.
Late caption
Copy fades in once the tile is fullscreen.
Soft exits
Neighbors fade to 15%, not to nothing.
Layered hero
z-index keeps traffic behind the center.
Reversible dive
Scrub back to reassemble the grid.

About this UI Snippet

Scroll Grid Zoom — Expand the Center Tile to Fullscreen on Scroll

Screenshot of the Scroll Grid Zoom snippet rendered live

The scroll grid zoom starts as an ordinary 3×3 image grid, then — as you scroll — the center tile grows until it owns the entire viewport while its eight neighbors accelerate outward and fade, as if the camera dove into the middle cell. It's a cinematic way to promote one item out of a collection. This snippet builds it with GSAP ScrollTrigger (from a CDN), a measured scale factor, and per-tile direction math.

Neighbors fly along their own grid direction

Each outer tile computes a direction vector from its grid index: col = (i % 3) − 1 and row = floor(i / 3) − 1 yield −1/0/+1 offsets that point directly away from the center cell. The corner tiles travel diagonally, edge tiles straight out — multiplied by ~85% of the viewport so everything clears the screen. Deriving direction from index means the scatter pattern is symmetric automatically, with no per-tile configuration.

The zoom factor is measured, not hardcoded

Instead of guessing "scale: 3.4", the script measures the center tile's rendered size with getBoundingClientRect() and computes max(vw/w, vh/h) × 1.05 — the exact scale at which the tile covers the viewport in both dimensions, with 5% overshoot to hide edge slivers. Because the grid is fluid (min(720px, 90vw)), a hardcoded factor would under- or over-shoot at different screen widths; measuring makes the effect resolution-independent.

Scale beats width/height animation here

The center tile grows via transform: scale(), not by animating its width and height. Transforms run on the compositor without reflowing the grid — animating the box size would relayout all nine tiles every frame and shift the grid as the middle cell grew. The visual cost of scaling (its contents grow too) is actually desirable: the tile's emoji becomes the hero image of the fullscreen view.

Everything shares timeline position 0

The eight fly-outs and the center zoom all start at position 0 on one scrubbed timeline, so the dive reads as a single camera move rather than a sequence. The caption is the only late element — it fades in at 70% of the timeline, once the tile has effectively become a fullscreen hero, turning the end state into a legitimate section header.

Fading, not removing, the neighbors

Outer tiles land at opacity: 0.15 rather than 0. During the scrub-back they're already faintly visible mid-viewport, which makes the reverse animation read as tiles returning rather than materializing from nothing. A touch of rotation (col × 6°) adds spin to the exits so the scatter feels thrown, not slid.

The center stays above the traffic

z-index: 2 on the center tile keeps neighbors passing *behind* it as they exit, preserving the illusion that the camera moves toward the middle cell instead of tiles crossing over the hero.

Customizing it

Swap emoji tiles for <img> or background-image cells, tune the exit distance, or zoom a different cell by changing which tile gets measured. Pair it with a scroll zoom hero for a single-image cousin, scroll image mask for a clip-path take, or a scroll reveal grid to rebuild a grid in the next section.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSA 3×3 tile grid renders with a marked center cell.
  3. 3
    Scroll into the stageNeighbors fly outward as the center starts growing.
  4. 4
    Reach fullscreenThe tile covers the viewport and its caption fades in.
  5. 5
    Scroll back upThe tile shrinks into its cell; neighbors return.
  6. 6
    Swap in imagesReplace the emoji tiles with img or background cells.

Real-world uses

Common Use Cases

Portfolio highlights
Dive into a featured project from the grid; browse the rest with a photo gallery.
Product heroes
Promote one product to fullscreen, then continue with sticky scroll features.
Travel and editorial
Zoom into a destination tile inside a scroll pin story.
Category landings
Enter a category cinematically; a scroll gallery pin can carry the detail.
Campaign reveals
Make the announcement the center cell, echoing a scroll zoom hero.
App screenshots
Expand the key screen from a feature grid like scroll reveal grid.
Related: Scroll-Scrubbed GLB Motion Trail
See the Scroll-Scrubbed GLB Motion Trail for a related scroll pattern worth pairing with this one.
Related: Scroll-Linked Audio Waveform Scrub
See the Scroll-Linked Audio Waveform Scrub for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Direction comes from each tile's grid index: col = (i % 3) − 1 and row = floor(i / 3) − 1 produce −1/0/+1 vectors pointing straight away from the center cell. Corners travel diagonally and edges straight out, multiplied by ~85% of the viewport so they fully clear the screen. No per-tile configuration is needed.

The script measures the tile's rendered size with getBoundingClientRect, then computes max(innerWidth/width, innerHeight/height) × 1.05 — the exact factor at which it covers the viewport both ways, with 5% overshoot to hide edge slivers. Because the grid is fluid, a hardcoded scale would misfire at other screen widths.

Scaling is a transform, so it composites on the GPU without touching layout. Animating the box's width/height would reflow the entire 3×3 grid every frame and physically shift the other cells while they're mid-exit. The side effect of scale — the tile's contents grow too — is exactly what a zoom should look like.

Yes — point the center variable at any tile and skip it in the exit loop; the measured scale factor still works. For a corner tile, add x/y compensation in the same tween (moving its center toward the viewport center as it scales), since scaling alone expands around the tile's own position, not the screen's middle.

Render tiles from an array and build the timeline in a mount effect (useEffect, onMounted, or ngAfterViewInit) within gsap.context, reverting on cleanup so the pin unregisters. Measure the center ref after layout (the effect runs post-render, so getBoundingClientRect is safe), and re-create the trigger on resize if your grid is fluid. The grid maps to Tailwind's grid utilities directly.