April 8, 2026/Advanced/Page Transitions

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.

Home

"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>
  );
}
  • gsap

Related Components

1W AGO
Signifly Theme Fadepage-transitions
3MO AGO
Section Transition Systempage-transitions
3MO AGO
Shutter Sectionpage-transitions