Add elegant page transition overlay using 3 staggered color layers. Overlay covers screen during navigation and reveals once new page is loaded. Use during /init or standalone.
Add an elegant overlay transition with 3 staggered layers. The overlay covers the screen during navigation and only reveals once the new page is fully loaded.
components/ui/page-transition.tsxCreate website/components/ui/page-transition.tsx:
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { usePathname } from "next/navigation";
import { useEffect, useState, useRef } from "react";
export function PageTransition() {
const pathname = usePathname();
const [isTransitioning, setIsTransitioning] = useState(false);
const [showOverlay, setShowOverlay] = useState(false);
const previousPathname = useRef(pathname);
// 3 layers with staggered delays - solid colors
const layers = [
{ bg: "bg-muted", delay: 0 },
{ bg: "bg-muted/95", delay: 0.08 },
{ bg: "bg-muted/90", delay: 0.16 },
];
const transitionDuration = 0.4;
const totalEnterTime = (transitionDuration + 0.16) * 1000; // duration + max delay
useEffect(() => {
// When pathname changes, start the transition
if (pathname !== previousPathname.current) {
setIsTransitioning(true);
setShowOverlay(true);
// Wait for enter animation to complete, then start exit
const exitTimer = setTimeout(() => {
setIsTransitioning(false);
previousPathname.current = pathname;
}, totalEnterTime + 100); // Small buffer for page render
// Hide overlay after exit animation completes
const hideTimer = setTimeout(() => {
setShowOverlay(false);
}, totalEnterTime + 100 + (transitionDuration + 0.16) * 1000);
return () => {
clearTimeout(exitTimer);
clearTimeout(hideTimer);
};
}
}, [pathname, totalEnterTime]);
if (!showOverlay) return null;
return (
<div className="pointer-events-none">
{layers.map((layer, index) => (
<motion.div
key={index}
className={`fixed inset-0 z-[999] ${layer.bg}`}
initial={{ scaleX: 0 }}
animate={{ scaleX: isTransitioning ? 1 : 0 }}
transition={{
duration: transitionDuration,
delay: layer.delay,
ease: [0.4, 0, 0.2, 1],
}}
style={{
originX: isTransitioning ? 0 : 1,
transformOrigin: isTransitioning ? "left" : "right",
}}
/>
))}
</div>
);
}
Add PageTransition to app/[locale]/layout.tsx - it renders as an overlay, not wrapping content:
import { PageTransition } from "@/components/ui/page-transition";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
export default function LocaleLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<PageTransition />
<Navbar />
<main>{children}</main>
<Footer />
</>
);
}
Important:
fixed positioning with z-[999] to overlay everything including modalspointer-events-none ensures it doesn't block interactionsAdd reduced motion support to globals.css:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
The key difference from basic transitions: the overlay waits at fullscreen state until the page is ready, then reveals.
Adjust the bg values for different looks:
// Default - solid muted with subtle variations
const layers = [
{ bg: "bg-muted", delay: 0 },
{ bg: "bg-muted/95", delay: 0.08 },
{ bg: "bg-muted/90", delay: 0.16 },
];
// Primary accent
const layers = [
{ bg: "bg-primary", delay: 0 },
{ bg: "bg-primary/95", delay: 0.08 },
{ bg: "bg-primary/90", delay: 0.16 },
];
// Secondary
const layers = [
{ bg: "bg-secondary", delay: 0 },
{ bg: "bg-secondary/95", delay: 0.08 },
{ bg: "bg-secondary/90", delay: 0.16 },
];
// Faster (snappy)
const transitionDuration = 0.3;