April 8, 2026/Intermediate/Page 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.

Component Vault

No-5 Studio

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

Related Components

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