April 8, 2026IntermediatePage Transitions
Pixel Curtain Transition
A directional variant of the pixel blocks transition. On open, 20 columns of pixel blocks sweep in left-to-right using the column index as delay (0.05s × col). On close, they sweep out right-to-left using the reversed index — creating a curtain effect.
Preview
Component Vault
No-5 Studio
Source
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import styles from "./styles.module.css";
const NUM_COLS = 20;
// delay[0] drives the open sweep, delay[1] drives the close sweep
const blockAnim = {
initial: { opacity: 0 },
open: (d) => ({ opacity: 1, transition: { duration: 0, delay: 0.05 * d[0] } }),
closed: (d) => ({ opacity: 0, transition: { duration: 0, delay: 0.05 * d[1] } }),
};
const menuAnim = {
initial: { opacity: 0 },
open: { opacity: 1, transition: { delay: 1.1, duration: 0.3 } },
closed: { opacity: 0, transition: { duration: 0.15 } },
};
export default function PixelCurtainTransition({ color = "#111111" }) {
const [isOpen, setIsOpen] = useState(false);
const [numBlocks, setNumBlocks] = useState(0);
useEffect(() => {
const calc = () => {
const blockSize = window.innerWidth * 0.05; // 5vw per column
setNumBlocks(Math.ceil(window.innerHeight / blockSize));
};
calc();
window.addEventListener("resize", calc);
return () => window.removeEventListener("resize", calc);
}, []);
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 curtain — columns sweep left→right on open, right→left on close */}
{numBlocks > 0 && (
<div className={styles.pixelBg} aria-hidden="true">
{Array.from({ length: NUM_COLS }, (_, colIdx) => (
<div key={colIdx} className={styles.column}>
{Array.from({ length: numBlocks }, (_, blockIdx) => (
<motion.div
key={blockIdx}
className={styles.block}
variants={blockAnim}
// [openDelay, closeDelay]: open sweeps left→right, close sweeps right→left
custom={[colIdx, NUM_COLS - 1 - colIdx]}
initial="initial"
animate={isOpen ? "open" : "closed"}
style={{ backgroundColor: color }}
/>
))}
</div>
))}
</div>
)}
{/* Menu */}
<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>
);
}
Dependencies
framer-motion