Next.js page transition with entry and exit animations using GSAP

Page transitions can instantly make your Next.js app feel smoother and more professional.In this guide, we’ll build a clean and simple overlay-based page transition:

  • Exit animation runs before navigation
  • Entry animation runs after navigation
  • Works across all routes
  • No styling — pure logic only

1.How it Works

1. A Transition Provider

  • Renders overlay
  • Runs GSAP animations
  • Exposes a navigate() function through React Context

2. A Transition Link

  • Calls navigate() instead of directly routing

3. Wrapping the entire app

  • So transitions run between all page

This approach works with the App Router.

2.The Transition Provider (Core Logic)

This component controls:

  • Initial GSAP setup
  • Exit animation
  • Navigation
  • Entry animation

Transition Provider:

ts
"use client";
import { useRouter, usePathname } from "next/navigation";
import gsap from "gsap";
import React, {
createContext,
useRef,
useLayoutEffect,
useEffect,
} from "react";
export const TransitionContext = createContext<{ navigate: (href: string) => void }>({ navigate: () => {} });
const TransitionProvider = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const pathname = usePathname();
const containerRef = useRef<HTMLDivElement>(null);
const isAnimating = useRef(false);
const firstLoad = useRef(true);
// Start off-screen
useLayoutEffect(() => {
gsap.set(containerRef.current, { y: "100%" });
}, []);
// Exit animation → navigate
const navigate = (href: string) => {
if (isAnimating.current) return;
isAnimating.current = true;
gsap.fromTo(containerRef.current,
{ y: "100%" },
{
y: "0%",
duration: 0.6,
ease: "power2.inOut",
onComplete: () => {
router.push(href);
if (href === pathname) {
requestAnimationFrame(() => enter());
}
},
}
);
};
// Entry animation
const enter = () => {
gsap.fromTo(containerRef.current,
{ y: "0%" },
{
y: "100%",
duration: 0.6,
ease: "power2.inOut",
onComplete: () => {
isAnimating.current = false;
},
}
);
};
// Run entry on every route change except first load
useEffect(() => {
if (firstLoad.current) {
firstLoad.current = false;
return;
}
enter();
}, [pathname]);
return (
<TransitionContext.Provider value={{ navigate }}>
{children}
{/* Overlay*/}
<div ref={containerRef} className="fixed inset-0 flex pointer-events-none bg-zinc-900 min-h-screen w-full" />
</TransitionContext.Provider>
);
};
export default TransitionProvider;

3.Transition Link Component

This replaces <Link> so we can run animations before navigating.

Transition Link:

ts
"use client";
import { useContext } from "react";
import { TransitionContext } from "./TransitionProvider";
const TransitionLink = ({ href, children }: { href: string; children: React.ReactNode }) => {
const { navigate } = useContext(TransitionContext);
return (
<button onClick={() => navigate(href)}>
{children}
</button>
);
};
export default TransitionLink;

4.Wrap Your App with the Provider

Wrap your app with the provider. This makes transitions work globally.

layout.tsx:

ts
import TransitionProvider from "@/components/Transition/TransitionProvider";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<TransitionProvider>{children}</TransitionProvider>
</body>
</html>
);
}

5.Example Usage on a Page

page.tsx:

ts
import TransitionLink from "@/components/Transition/TransitionLink";
export default function Home() {
return (
<div>
<h1>Home Page</h1>
<TransitionLink href="/about">Go to About</TransitionLink>
</div>
);
}

Final Result

With just a small setup, you now have:

✔ Simple overlay page transitions

✔ Smooth exit + entry animations

✔ Reusable <TransitionLink>

✔ No complex styling required

✔ Automatically works across all routes

This is a minimal, clean, production-ready GSAP transition system for Next.js App Router.

Video preview

MALAY

Crafted at 2AM by MalayPatel

“Powered by creativity, fueled by caffeine and created in dark mode.”

. All rights reserved.