April 8, 2026/Intermediate/Page Transitions

Stairs Wipe

5 full-height columns cascade down in a staircase sequence wiping the screen black, then reveal the new page. Each column has a custom delay of 0.05 * (5 - index) for the staircase order.

Home

"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import styles from "./styles.module.css";

const PAGES = [
  { key: "home", label: "Home", bg: "#f5f5f5", color: "#1a1a1a" },
  { key: "about", label: "About", bg: "#0d0d0d", color: "#ffffff" },
  { key: "work", label: "Work", bg: "#1a1f2e", color: "#ffffff" },
];

const NUM_COLUMNS = 5;

function getStepVariants(custom) {
  return {
    initial: { top: "-100%" },
    enter: { top: "-100%", transition: { duration: 0 } },
    exit: {
      top: 0,
      transition: {
        duration: 0.4,
        ease: [0.76, 0, 0.24, 1],
        delay: 0.05 * custom,
      },
    },
  };
}

export default function StairsWipe() {
  const [pageIndex, setPageIndex] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);

  const page = PAGES[pageIndex];

  function navigate(nextIndex) {
    if (isAnimating || nextIndex === pageIndex) return;
    setIsAnimating(true);
    setPageIndex(nextIndex);
    setTimeout(() => setIsAnimating(false), 900);
  }

  return (
    <div className={styles.demo}>
      <div className={styles.viewport}>
        {/* Staircase columns */}
        <AnimatePresence>
          {Array.from({ length: NUM_COLUMNS }).map((_, i) => (
            <motion.div
              key={`col-${pageIndex}-${i}`}
              className={styles.column}
              style={{ left: `${(i / NUM_COLUMNS) * 100}%`, width: `${100 / NUM_COLUMNS}%` }}
              custom={NUM_COLUMNS - i}
              variants={getStepVariants(NUM_COLUMNS - i)}
              initial="initial"
              animate="enter"
              exit="exit"
            />
          ))}
        </AnimatePresence>

        {/* Page content */}
        <AnimatePresence mode="wait">
          <motion.div
            key={`page-${pageIndex}`}
            className={styles.page}
            style={{ background: page.bg, color: page.color }}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1, transition: { delay: 0.4, duration: 0.3 } }}
            exit={{ opacity: 0, transition: { duration: 0.1 } }}
          >
            <h2 className={styles.pageTitle}>{page.label}</h2>
          </motion.div>
        </AnimatePresence>
      </div>

      <div className={styles.nav}>
        {PAGES.map((p, i) => (
          <button
            key={p.key}
            className={`${styles.navBtn} ${i === pageIndex ? styles.active : ""}`}
            onClick={() => navigate(i)}
          >
            {p.label}
          </button>
        ))}
      </div>
    </div>
  );
}
  • framer-motion

Related Components

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