April 8, 2026IntermediatePage 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.
Preview
Source
"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>
);
}
Dependencies
framer-motion