AnimationsIntermediateApril 8, 2026
Pixel Blocks Transition
A menu overlay built from 20 columns of square pixel blocks. When toggled, blocks fade in with shuffled random delays (Fisher-Yates) creating a scattered fill effect. The menu fades in after the pixels settle, then reverses on close.
View Full Demo →Preview
Component Vault
No-5 Studio
Source
index.jsx
"use client";
import { useState, useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import styles from "./styles.module.css";
const NUM_COLS = 20;
// Fisher-Yates shuffle — returns a new array
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
const blockAnim = {
initial: { opacity: 0 },
open: (delay) => ({ opacity: 1, transition: { duration: 0, delay: 0.03 * delay } }),
closed: (delay) => ({ opacity: 0, transition: { duration: 0, delay: 0.03 * delay } }),
};
const menuAnim = {
initial: { opacity: 0 },
open: { opacity: 1, transition: { delay: 0.5, duration: 0.3 } },
closed: { opacity: 0, transition: { duration: 0.15 } },
};
export default function PixelBlocksTransition({ color = "#111111" }) {
const [isOpen, setIsOpen] = useState(false);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const update = () =>
setDimensions({ width: window.innerWidth, height: window.innerHeight });
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
// Pre-compute stable shuffled delay indexes for all 20 columns
const columnDelays = useMemo(() => {
if (!dimensions.width) return [];
const blockSize = dimensions.width * 0.05;
const numBlocks = Math.ceil(dimensions.height / blockSize);
return Array.from({ length: NUM_COLS }, () =>
shuffle(Array.from({ length: numBlocks }, (_, i) => i))
);
}, [dimensions.width, dimensions.height]);
return (
<div className={styles.demo}>
{/* Page content */}
<div className={styles.page}>
<h2 className={styles.pageTitle}>Component Vault</h2>
<p className={styles.pageSub}>No-5 Studio</p>
</div>
{/* Burger toggle */}
<button
className={`${styles.burger} ${isOpen ? styles.burgerOpen : ""}`}
onClick={() => setIsOpen((v) => !v)}
aria-label="Toggle menu"
>
<span /><span /><span />
</button>
{/* Pixel block overlay */}
{columnDelays.length > 0 && (
<div className={styles.pixelBg} aria-hidden="true">
{columnDelays.map((delays, colIdx) => (
<div key={colIdx} className={styles.column}>
{delays.map((delay, blockIdx) => (
<motion.div
key={blockIdx}
className={styles.block}
variants={blockAnim}
custom={delay}
initial="initial"
animate={isOpen ? "open" : "closed"}
style={{ backgroundColor: color }}
/>
))}
</div>
))}
</div>
)}
{/* Menu — appears after pixels fill in */}
<motion.nav
className={styles.menu}
variants={menuAnim}
initial="initial"
animate={isOpen ? "open" : "closed"}
>
{["Work", "About", "Lab", "Contact"].map((item) => (
<p key={item} className={styles.menuItem}>{item}</p>
))}
</motion.nav>
</div>
);
}
styles.module.css
.demo {
position: relative;
width: 100%;
height: 100dvh;
overflow: hidden;
background: #f5f5f0;
color: #111;
}
/* ── Page content ── */
.page {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.pageTitle {
font-size: clamp(2.5rem, 8vw, 6rem);
font-weight: 700;
letter-spacing: -0.04em;
margin: 0;
}
.pageSub {
font-size: 1rem;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.4;
margin: 0;
}
/* ── Burger button ── */
.burger {
position: absolute;
top: 1.5rem;
right: 1.5rem;
z-index: 200;
width: 2.5rem;
height: 2.5rem;
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.burger span {
display: block;
width: 100%;
height: 1.5px;
background: #111;
transform-origin: center;
transition: transform 0.3s ease, opacity 0.2s;
}
.burgerOpen span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); background: #f5f5f0; }
.burgerOpen span:nth-child(2) { opacity: 0; background: #f5f5f0; }
.burgerOpen span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); background: #f5f5f0; }
/* ── Pixel overlay ── */
.pixelBg {
position: absolute;
inset: 0;
display: flex;
z-index: 50;
pointer-events: none;
overflow: hidden;
}
.column {
flex: 1;
display: flex;
flex-direction: column;
}
.block {
width: 100%;
aspect-ratio: 1;
}
/* ── Menu ── */
.menu {
position: absolute;
inset: 0;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
pointer-events: none;
}
.menuItem {
font-size: clamp(2.5rem, 7vw, 5rem);
font-weight: 700;
letter-spacing: -0.04em;
color: #f5f5f0;
margin: 0;
cursor: pointer;
pointer-events: all;
transition: opacity 0.2s;
}
.menuItem:hover {
opacity: 0.5;
}
Dependencies
framer-motion