All components

Block Reveal Page Transition

GSAP animation component · Published 2026-07-21 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Page transition where a fixed fullscreen grid of 60px black blocks flickers in with a random stagger to cover the viewport, the page content swaps behind it, then the blocks fade back out to reveal the new page. Triggered by clicking the bottom nav links that route between four full-viewport sci-fi sections.

How it's built

Categorytransition
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

page-transition blocks pixelated stagger random fake-router grid

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Block Reveal Page Transition (pixel-grid flicker cover-and-reveal)

Goal

Build a client-side "fake router" for a small four-page site where navigating between pages plays a full-screen grid of small dark blocks that flickers in with a random stagger to fully cover the viewport, swaps the page content underneath while covered, then flickers the blocks back out to reveal the new page. The star effect is the two-phase random-stagger opacity flicker of a procedurally generated 60px block grid acting as a transition curtain. Trigger is a click on the fixed bottom nav links.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no ScrollTrigger, no SplitText, no Lenis. There is no scroll interaction at all. Import as:

import gsap from "gsap";

No gsap.registerPlugin call is needed.

Layout / HTML

.transition-grid            (fixed full-viewport overlay; JS fills it with blocks)

nav                         (fixed bottom-center pill row)
  a[data-route="/"]         "Homebase"
  a[data-route="/gateway"]  "Gateway"
  a[data-route="/station"]  "Station"
  a[data-route="/colony"]   "Colony"

section.page.active[data-route="/"]          (Homebase — first page, visible on load)
  .section-bg > img         (full-bleed background image)
  .section-header > h1      "Homebase"
section.page[data-route="/gateway"]          (Gateway)
  .section-bg > img
  .section-header > h1      "Gateway"
section.page[data-route="/station"]          (Station)
  .section-bg > img
  .section-header > h1      "Station"
section.page[data-route="/colony"]           (Colony)
  .section-bg > img
  .section-header > h1      "Colony"
  • The .transition-grid starts empty in the HTML — JS builds and appends all the block <div>s.
  • Each nav link carries a data-route matching a section.page's data-route. Only one section.page has class active at a time.
  • Use neutral fictional page names (four sci-fi place labels like Homebase / Gateway / Station / Colony). No real brands.

Styling

Fonts:

  • Display: SCHABO (condensed uppercase display face, via @import url("https://fonts.cdnfonts.com/css/schabo")), used for h1.
  • UI: DM Mono (Google Fonts, weights 300/400/500 + italics), used for nav links.

Color tokens:

  • --base-100: #fcfcfc (near-white text)
  • --base-200: #3f3f3f (mid-grey page background, seen only briefly)
  • --base-300: #0f0f0f (near-black — the transition blocks AND the nav pill background)
  • body { background-color: var(--base-200); color: var(--base-100); }

Type:

  • h1: text-transform: uppercase; font-family:"SCHABO", sans-serif; font-size: clamp(6rem, 15vw, 12rem); font-weight:500; line-height:0.9;
  • a: text-transform: uppercase; color: var(--base-100); font-family:"DM Mono", monospace; font-size:0.8rem; font-weight:450; line-height:1; text-decoration:none;
  • img { width:100%; height:100%; object-fit: cover; }

Key structural CSS (load-bearing):

  • nav: position: fixed; bottom: 5rem; left: 50%; transform: translateX(-50%); display:flex; gap:0.25rem; z-index:2;
  • nav a: padding:0.5rem 0.75rem; background-color: var(--base-300); border-radius:0.2rem; transition: transform 200ms ease-out; and nav a:active { transform: scale(0.9); } (a small press feedback on click).
  • section: position:relative; width:100%; height:100svh; display:flex; justify-content:center; align-items:center; overflow:hidden;
  • Fake-router visibility: section.page { display:none; } and section.page.active { display:flex; } — only the active page is in the layout; the others are removed entirely.
  • .section-bg: position:absolute; width:100%; height:100%; z-index:-1; (image sits behind the centered h1).
  • .transition-grid: position:fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:100; overflow:hidden; — it sits above everything (nav is z-index 2, grid is z-index 100) and never blocks clicks.
  • .transition-block: position:absolute; background-color: var(--base-300); opacity:0; — every block is absolutely positioned by inline left/top/width/height from JS and starts fully transparent.

GSAP effect (exhaustive)

Constants & grid generation
  • const BLOCK_SIZE = 60; — each block is a 60×60px square.
  • createTransitionGrid() builds the curtain:
  • gridWidth = window.innerWidth, gridHeight = window.innerHeight.
  • columns = Math.ceil(gridWidth / BLOCK_SIZE).
  • rows = Math.ceil(gridHeight / BLOCK_SIZE) + 1 (one extra row so the bottom edge is always covered).
  • offsetX = (gridWidth - columns * BLOCK_SIZE) / 2 and offsetY = (gridHeight - rows * BLOCK_SIZE) / 2 — centers the grid (offsets are ≤ 0, so the grid slightly overhangs to guarantee full coverage).
  • Nested loop row × col: create div.transition-block, set inline width/height = 60px, left = col*60 + offsetX, top = row*60 + offsetY, append to .transition-grid, and push into a blocks[] array.
  • Before generating, clear the grid: transitionGrid.innerHTML = "" and reset blocks = [].
  • After building, gsap.set(blocks, { opacity: 0 }).
  • Call createTransitionGrid() once on load, and rebuild it on window.resize (window.addEventListener("resize", createTransitionGrid)).
The two transition tweens (this is the whole effect)

Two functions, each a single gsap.to(blocks, …) over the block array, each with an onComplete callback (next) to chain the sequence:

leave(next) — cover the screen:

gsap.to(blocks, {
  opacity: 1,
  duration: 0.05,
  ease: "power2.inOut",
  stagger: { amount: 0.5, from: "random" },
  onComplete: next,
});
  • Every block animates opacity: 0 → 1.
  • Per-block duration is only 0.05s (a fast flicker-on), but the whole set is spread across stagger: { amount: 0.5, from: "random" } — total stagger window of 0.5s, and from:"random" shuffles the order so blocks light up in a scattered, dissolve-in pattern rather than any directional sweep. Net wall-clock time ≈ 0.55s.

enter(next) — reveal the new page:

gsap.set(blocks, { opacity: 1 });          // ensure fully covered first
gsap.to(blocks, {
  opacity: 0,
  duration: 0.05,
  delay: 0.3,
  ease: "power2.inOut",
  stagger: { amount: 0.5, from: "random" },
  onComplete: next,
});
  • First force all blocks to opacity: 1 (fully opaque cover), then tween opacity: 1 → 0.
  • Same duration: 0.05, same ease: "power2.inOut", same stagger: { amount: 0.5, from: "random" }, plus a delay: 0.3 — a 0.3s hold on full black before the blocks flicker back out (this pause is what hides the content swap). Net wall-clock time ≈ 0.85s.
Sequencing (navigate(route))

State: currentRoute = "/", isTransitioning = false.

  • Guard: if isTransitioning or route === currentRoute, return (ignore re-clicks and clicks on the current page).
  • Set isTransitioning = true, then:
  • leave(() => { … }) — blocks flicker IN to cover.
  • In leave's onComplete: showPage(route) (toggle .active so the new section.page becomes the only visible page) and currentRoute = route. The swap happens while the screen is fully covered.
  • Then call enter(() => { isTransitioning = false; }) — after the 0.3s hold, blocks flicker OUT to reveal, and re-entrancy unlocks on complete.
  • showPage(route): loop all .page sections, page.classList.toggle("active", page.dataset.route === route).
Trigger wiring

For every nav a, addEventListener("click", e => { e.preventDefault(); navigate(link.dataset.route); }). (No History API / URL change — it is a purely visual fake router.)

Assets / images

4 images, one full-bleed background per page (.section-bg > img, object-fit: cover), all 16:9 landscape (~1456×816) to fill the viewport. Role: atmospheric full-screen sci-fi scene behind each page's giant h1. Each has a distinct dominant color so the page swap is obvious through the flicker. The four, in nav order:

  • Homebase (bg_1): curved silver/chrome futuristic towers being swallowed by a churning orange-amber sandstorm on a desert world; wrecked pods half-buried in the foreground. Dominant colors: burnt orange, tan, glints of steel grey.
  • Gateway (bg_2): a glowing golden energy portal (a rectangular doorway with a swirling vortex) framed by two tall rocky spires under a teal starry night sky, with turquoise crystals scattered on the ground and stone steps leading up. Dominant colors: deep teal/green sky vs warm gold portal glow.
  • Station (bg_3): a glass geodesic dome colony lit up from within, sitting on a red-rock Mars-like desert; a huge mottled orange planet looms in a green-tinged starry sky. Dominant colors: rust red, amber dome lights, teal-green sky.
  • Colony (bg_4): black silhouetted spires and towers of a dense city against an enormous red-orange sun/star, with small ships drifting through smoky haze. Dominant colors: fiery red-orange over near-black silhouettes.

Provide 4 distinct landscape images matching these roles, forms and color palettes (no real brands).

Behavior notes

  • Desktop and mobile both work; there is no prefers-reduced-motion branch and no min-width gate.
  • The grid is regenerated on every resize, so the block count adapts to the viewport; gsap.set re-zeroes opacity each rebuild.
  • .transition-grid has pointer-events: none, so even at full black cover the nav underneath (z-index 2 vs grid z-index 100) can't be clicked — but the isTransitioning guard already blocks input during a transition.
  • No infinite loops; each animation is one-shot per click. Total transition ≈ 1.4s door-to-door (leave ~0.55s + 0.3s hold + reveal ~0.5s).
  • The first page (/ Homebase) is visible on load with no intro animation; transitions only fire on nav clicks.