How to build page transitions in Next.js using the app directory

November 19, 2024

Why AnimatePresence no longer works in the app directory

With the app directory, Next.js moves toward server components by default, rendering the entire component tree on the server and allowing better performance optimizations. However, this impacts client-only libraries like Framer Motion. Specifically, AnimatePresence often struggles to retain the component hierarchy it needs, causing the transitions to be interrupted or not play at all.

  1. The App Router uses React Server Components by default
  2. Page transitions are handled differently in the new routing system
  3. AnimatePresence expects components to remain mounted during the animation

To work around this limitation, we can use Next.js’s next/navigation to control the routing directly on the client. This approach offers several advantages:

  • Works smoothly with the App Router
  • Lighter weight than Framer Motion
  • More control over the transition timing
  • Better performance

Create the Transition Component

First, let's create a new file called Transition.jsx in your components directory:

Transition.jsx

jsx

1"use client";
2
3import Link from "next/link";
4import { useRouter } from "next/navigation";
5
6const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
8export const Transition = ({ children, href, className = "", ...props }) => {
9    const router = useRouter();
10
11    const handleTransition = async (e) => {
12        e.preventDefault();
13
14        // Start the transition
15        document.body.classList.add("page-transition");
16
17        // Wait for the animation to play
18        await sleep(500);
19
20        // Navigate to the new page
21        router.push(href);
22
23        // Wait for the page to load and remove the transition class
24        await sleep(500);
25            document.body.classList.remove("page-transition");
26        };
27
28    return (
29        <Link 
30            className={className}
31            href={href} 
32            onClick={handleTransition}
33            {...props}
34        >
35            {children}
36        </Link>
37    );
38};

Add the CSS Transitions

Create or update your global CSS file:

global.css

css

1/* Base styles */
2body {
3  opacity: 1;
4  transition: opacity 0.5s ease-in-out;
5}
6
7/* Transition styles */
8body.page-transition {
9  opacity: 0;
10}
11
12/* Optional: Add different transition effects */
13@keyframes fadeOut {
14  from { opacity: 1; }
15  to { opacity: 0; }
16}
17
18@keyframes fadeIn {
19  from { opacity: 0; }
20  to { opacity: 1; }
21}
22

Using the TransitionLink Component

Now you can use the TransitionLink component in your pages:

page.js

jsx

1import { Transition } from "@/components/Transition";
2
3export default function HomePage() {
4  return (
5    <div className="p-4">
6      <h1>Welcome to My Site</h1>
7      <Transition href="/about">
8        Go to About Page
9      </Transition>
10    </div>
11  );
12}
13

How It Works

Let's break down how this solution works:

  1. Initialization: When a user clicks a Transition, the handleTransition function is triggered instead of the default Link behavior.

  2. Transition Start:

    • The function adds a page-transition class to the body
    • This triggers the CSS opacity transition, fading out the current page
  3. Navigation:

    • After waiting for the fade-out animation (500ms)
    • The router pushes the new route
    • Another wait ensures the new page has time to load
  4. Transition End:

    • The page-transition class is removed
    • The page fades back in due to the CSS transition

Best Practices

  1. Performance: Keep transitions short (300-500ms) to maintain a snappy feel
  2. Accessibility: Ensure transitions respect user preferences (prefers-reduced-motion)
  3. Error Handling: Add try/catch blocks for navigation errors
  4. Loading States: Show loading indicators for slower transitions

Common Issues and Solutions

  1. Flickering: If you notice flickering, try adding will-change: opacity to the transitioning elements
  2. Multiple Clicks: Disable the link during transition
  3. Memory Leaks: Clean up any animations if the component unmounts

This solution provides a way to implement page transitions in Next.js App Router without relying on heavy animation libraries. It's customizable, performant, and works well with the new routing system. Remember to test your transitions across different devices and network conditions to ensure a smooth user experience.