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-gridstarts empty in the HTML — JS builds and appends all the block<div>s. - Each nav link carries a
data-routematching asection.page'sdata-route. Only onesection.pagehas classactiveat 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 forh1. - 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;andnav 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; }andsection.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 centeredh1)..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 inlineleft/top/width/heightfrom 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) / 2andoffsetY = (gridHeight - rows * BLOCK_SIZE) / 2— centers the grid (offsets are ≤ 0, so the grid slightly overhangs to guarantee full coverage).- Nested loop
row×col: creatediv.transition-block, set inlinewidth/height = 60px,left = col*60 + offsetX,top = row*60 + offsetY, append to.transition-grid, and push into ablocks[]array. - Before generating, clear the grid:
transitionGrid.innerHTML = ""and resetblocks = []. - After building,
gsap.set(blocks, { opacity: 0 }). - Call
createTransitionGrid()once on load, and rebuild it onwindow.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
durationis only 0.05s (a fast flicker-on), but the whole set is spread acrossstagger: { amount: 0.5, from: "random" }— total stagger window of 0.5s, andfrom:"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 tweenopacity: 1 → 0. - Same
duration: 0.05, sameease: "power2.inOut", samestagger: { amount: 0.5, from: "random" }, plus adelay: 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
isTransitioningorroute === currentRoute, return (ignore re-clicks and clicks on the current page). - Set
isTransitioning = true, then: leave(() => { … })— blocks flicker IN to cover.- In
leave'sonComplete:showPage(route)(toggle.activeso the newsection.pagebecomes the only visible page) andcurrentRoute = 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.pagesections,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-motionbranch and no min-width gate. - The grid is regenerated on every resize, so the block count adapts to the viewport;
gsap.setre-zeroes opacity each rebuild. .transition-gridhaspointer-events: none, so even at full black cover the nav underneath (z-index 2 vs grid z-index 100) can't be clicked — but theisTransitioningguard 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.