NEMAWASHI LAB

Stagger

GSAP

A 48-card grid that reveals and hides in batches as you scroll, powered by ScrollTrigger.batch. Cards entering the viewport animate from y:40, opacity:0, scale:0.85 with a 0.08s stagger and back.out(1.4) overshoot; leaving cards slide out in the opposite direction. The trigger zone spans from 90% top to 10% bottom, so transitions fire well before a card hits the edge. Each card's hue is seeded-random, giving a confetti palette without true randomness between loads.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

Source

StaggerReveal.tsx108 lines
"use client";

import { useRef, useEffect, useState } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import ScrambleLink from "../ScrambleLink";

gsap.registerPlugin(ScrollTrigger);

const GRID_SIZE = 48;

function seededRandom(seed: number) {
  const x = Math.sin(seed * 9301 + 49297) * 49297;
  return x - Math.floor(x);
}

export default function StaggerReveal() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [dark] = useState(true);

  useEffect(() => {
    document.documentElement.classList.add("allow-scroll");
    return () => document.documentElement.classList.remove("allow-scroll");
  }, []);

  useEffect(() => {
    if (!containerRef.current) return;

    const ctx = gsap.context(() => {
      ScrollTrigger.batch(".sr-card", {
        onEnter: (elements) => {
          gsap.fromTo(
            elements,
            { y: 40, opacity: 0, scale: 0.85 },
            { y: 0, opacity: 1, scale: 1, stagger: 0.08, duration: 0.6, ease: "back.out(1.4)", overwrite: true }
          );
        },
        onLeave: (elements) => {
          gsap.to(elements, { y: -20, opacity: 0, stagger: 0.05, duration: 0.3, overwrite: true });
        },
        onEnterBack: (elements) => {
          gsap.to(elements, { y: 0, opacity: 1, scale: 1, stagger: 0.05, duration: 0.4, ease: "power2.out", overwrite: true });
        },
        onLeaveBack: (elements) => {
          gsap.to(elements, { y: 40, opacity: 0, scale: 0.85, stagger: 0.05, duration: 0.3, overwrite: true });
        },
        start: "top 90%",
        end: "bottom 10%",
      });
    }, containerRef);

    return () => ctx.revert();
  }, []);

  return (
    <div
      ref={containerRef}
      style={{ background: "#0a0a0a", minHeight: "100vh", padding: "100vh 0" }}
    >
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
          gap: 16,
          padding: "0 32px",
          maxWidth: 900,
          margin: "0 auto",
        }}
      >
        {Array.from({ length: GRID_SIZE }, (_, i) => {
          const hue = (seededRandom(i) * 360) | 0;
          const lightness = dark ? 20 + seededRandom(i + 50) * 20 : 85 + seededRandom(i + 50) * 10;
          return (
            <div
              key={i}
              className="sr-card"
              style={{
                aspectRatio: "1",
                borderRadius: 12,
                background: `hsl(${hue}, 40%, ${lightness}%)`,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                opacity: 0,
              }}
            >
              <span
                style={{
                  fontFamily: "var(--font-flux), sans-serif",
                  fontVariationSettings: "'wght' 500, 'SRIF' 0",
                  fontSize: 16,
                  color: dark ? "rgba(255,255,255,0.6)" : "rgba(26,26,46,0.5)",
                }}
              >
                {String(i + 1).padStart(2, "0")}
              </span>
            </div>
          );
        })}
      </div>

      <div className="fixed bottom-6 left-6" style={{ zIndex: 20 }}>
        <ScrambleLink from="STAGGER" to="← LAB" href="/lab" className="text-[11px] tracking-[0.1em] text-foreground/30 uppercase hover:text-foreground/60 transition-colors" style={{ fontVariationSettings: "'wght' 400, 'SRIF' 100" }} />
      </div>
    </div>
  );
}

NEMAWASHI — Kotaro Abe