AnimationsAdvancedApril 8, 2026
Pixel Grid Transition
Full-viewport grid of colored blocks that fades in randomly on navigation and out on page load, creating a pixelated wipe effect. Grid density adapts to the container size.
View Full Demo →Preview
Home
Source
index.jsx
"use client";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import styles from "./styles.module.css";
const PAGES = [
{ key: "home", label: "Home", bg: "#f5f5f5", color: "#111111" },
{ key: "about", label: "About", bg: "#111111", color: "#f5f5f5" },
{ key: "work", label: "Work", bg: "#1a1f2e", color: "#e0e0e0" },
];
function calcGrid(el) {
const w = el.clientWidth;
const h = el.clientHeight;
const cols = w <= 479 ? 4 : w <= 767 ? 6 : 8;
const blockSize = w / cols;
const rows = Math.ceil(h / blockSize);
return { cols, rows, blockSize, total: cols * rows };
}
export default function PixelGridTransition({ color = "#ff4c24" }) {
const demoRef = useRef(null);
const overlayRef = useRef(null);
const isAnimating = useRef(false);
const entranceDone = useRef(false);
const [pageIndex, setPageIndex] = useState(0);
const [grid, setGrid] = useState({ cols: 8, rows: 0, blockSize: 0, total: 0 });
// Recalculate grid whenever the container resizes
useEffect(() => {
const el = demoRef.current;
if (!el) return;
const ro = new ResizeObserver(() => setGrid(calcGrid(el)));
ro.observe(el);
return () => ro.disconnect();
}, []);
// Entrance: play once after the first grid calculation
useEffect(() => {
if (entranceDone.current || grid.total === 0) return;
if (!overlayRef.current) return;
entranceDone.current = true;
const overlay = overlayRef.current;
const blocks = overlay.querySelectorAll("[data-block]");
overlay.style.display = "grid";
gsap.set(blocks, { autoAlpha: 1 });
gsap.to(blocks, {
autoAlpha: 0,
duration: 0.1,
delay: 0.3,
ease: "linear",
stagger: { amount: 0.75, from: "random" },
onComplete: () => { overlay.style.display = "none"; },
});
}, [grid.total]);
function navigate(nextIndex) {
if (isAnimating.current || nextIndex === pageIndex) return;
if (!overlayRef.current) return;
isAnimating.current = true;
const overlay = overlayRef.current;
const blocks = overlay.querySelectorAll("[data-block]");
overlay.style.display = "grid";
// Fade blocks IN → swap page → fade blocks OUT
gsap.fromTo(
blocks,
{ autoAlpha: 0 },
{
autoAlpha: 1,
duration: 0.001,
ease: "linear",
stagger: { amount: 0.5, from: "random" },
onComplete: () => {
setPageIndex(nextIndex);
gsap.to(blocks, {
autoAlpha: 0,
duration: 0.1,
delay: 0.15,
ease: "linear",
stagger: { amount: 0.75, from: "random" },
onComplete: () => {
overlay.style.display = "none";
isAnimating.current = false;
},
});
},
}
);
}
const page = PAGES[pageIndex];
return (
<div ref={demoRef} className={styles.demo}>
{/* Pixel overlay — position: absolute so it stays in the demo container */}
<div
ref={overlayRef}
className={styles.overlay}
style={{
gridTemplateColumns: `repeat(${grid.cols}, 1fr)`,
gridTemplateRows: `repeat(${grid.rows}, ${grid.blockSize}px)`,
}}
>
{Array.from({ length: grid.total }).map((_, i) => (
<div key={i} data-block="" className={styles.block} style={{ backgroundColor: color }} />
))}
</div>
{/* Page content */}
<div className={styles.page} style={{ background: page.bg, color: page.color }}>
<h2 className={styles.pageTitle}>{page.label}</h2>
</div>
{/* Nav */}
<nav className={styles.nav}>
{PAGES.map((p, i) => (
<button
key={p.key}
className={`${styles.navBtn} ${i === pageIndex ? styles.navBtnActive : ""}`}
onClick={() => navigate(i)}
>
{p.label}
</button>
))}
</nav>
</div>
);
}
styles.module.css
.demo {
position: relative;
width: 100%;
height: 100dvh;
overflow: hidden;
}
/* ── Pixel overlay ── */
.overlay {
position: absolute;
inset: 0;
display: none; /* shown/hidden via JS */
z-index: 100;
}
.block {
width: 100%;
height: 100%;
}
/* ── Page content ── */
.page {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.pageTitle {
font-size: clamp(2.5rem, 10vw, 7rem);
font-weight: 700;
letter-spacing: -0.04em;
margin: 0;
}
/* ── Nav ── */
.nav {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.5rem;
z-index: 10;
}
.navBtn {
padding: 0.45rem 1.1rem;
border: 1px solid rgba(128, 128, 128, 0.4);
background: rgba(128, 128, 128, 0.1);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: inherit;
cursor: pointer;
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
border-radius: 2rem;
transition: background 0.2s, border-color 0.2s;
}
.navBtn:hover {
background: rgba(128, 128, 128, 0.2);
}
.navBtnActive {
background: rgba(128, 128, 128, 0.3);
border-color: rgba(128, 128, 128, 0.7);
}
Dependencies
gsap