Page transitions and animations with Next.js + Framer [REPO]

The goal of this tutorial is to implement navigation animations in Next.js. The end result will be this subtle animation that will improve the appearance of your application.

I have created a codesandbox so you can analyze the result in the following link:

https://codesandbox.io/p/github/juannunezblasco/next-framer-motion/main

Now we are going to analyze step by step how to carry out the implementation.

Why include animations between routes in your Next.js application

In terms of user experience, animations are an essential element that provides feedback when an action is performed. Used to give the sensation of greater interactivity and provide information to the client during the loading of resources.

Additionally, animations can be used to increase the feeling of loading speed when loading times are high.

But in many cases we fall into the trap of making animations that only seek visual delight and demonstrate the skill of the developer behind it.

Whoever hasn’t done it should cast the first stone.

Let’s see how to implement page transitions and basic animations with Next.js and framer.

Base project configuration

For this tutorial I will use the base installation of Next.js with Typescript and Tailwind.

pnpm create next-app --typescript
➜  blog pnpm create next-app --typescript
.../Library/pnpm/store/v3/tmp/dlx-12645  |   +1 +
.../Library/pnpm/store/v3/tmp/dlx-12645  | Progress: resolved 1, reused 0, downloaded 1, added 1, done
✔ What is your project named? … next-framer-motion
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

All routes will share the same layout:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
<html lang="en">
<body className='min-h-dvh overflow-x-hidden flex flex-col'>
  <header className="py-8 bg-black flex flex-col items-center">
    <nav className="container flex items-center">
      <Link className="mr-4" href="/">Home</Link>
      <Link className="mr-4" href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  </header>
  <main>
    {children}
  </main>
  <footer></footer>
</body>
</html>
  );
}

Once the project is installed we will create the following routes to carry out the implementation:

  • Home
  • About
  • Blog

All of them will have a similar structure.

export default function Page() {
  return (
    <div className="bg-blue-500">
      <div className="container flex flex-col items-center content-center justify-between h-[90vh] mx-auto">
        <h1 className="text-white my-auto font-extrabold text-xl">Page</h1>
      </div>
    </div>
  );
}

Implement Framer Motion for Next.js

First we import the Framer library as indicated on their website:

pnpm add framer-motion

To implement this library we have to create several components.

First of all we have to create a context provider to manage the state of our application between transitions.

// context.tsx

"use client";

import { useRouter } from "next/navigation";
import { PropsWithChildren, createContext, use, useTransition } from "react";

export const DELAY = 400;

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
const noop = () => {};

type TransitionContext = {
  pending: boolean;
  navigate: (url: string) => void;
};
const Context = createContext<TransitionContext>({
  pending: false,
  navigate: noop,
});
export const useNavigationTransition = () => use(Context);

export default function Transitions({ children }: PropsWithChildren) {
  const [pending, start] = useTransition();
  const router = useRouter();
  const navigate = (href: string) => {
    start(async () => {
      await Promise.all([router.push(href), sleep(DELAY)]);
    });
  };

  return (
    <Context.Provider value={{ pending, navigate }}>
      {children}
    </Context.Provider>
  );
}

The sleep function will help us determine the duration of the transition. Which we will set through the DELAY constant.

Then we create a context with two variables:

  • Pending: Sets when the animation is running.
  • Navigate: a function where we manage the Next.js router

To implement Framer we have to create a component where we import AnimatePresence and motion from Framer.

// Navitate.tsx

"use client";

import { PropsWithChildren } from "react";
import { DELAY, useNavigationTransition } from "./context";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";

export default function Animate({ children }: PropsWithChildren) {
  const { pending } = useNavigationTransition();
  const pathname = usePathname();
  return (
    <AnimatePresence mode="popLayout">
      {!pending && (
        <motion.div
          key={pathname}
          className="flex-1"
          initial={{ x: 300, opacity: 0 }}
          animate={{ x: 0, opacity: 1 }}
          exit={{ x: 300, opacity: 0 }}
          transition={{ ease: "circInOut", duration: DELAY / 1000 }}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Now we include our component in the Layout that I showed you at the beginning of the article:

// layout.tsx

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className='min-h-dvh overflow-x-hidden flex flex-col'>
        <Transitions>
          <header className="py-8 bg-black flex flex-col items-center">
            <nav className="container flex items-center">
              <Link className="mr-4" href="/">Home</Link>
              <Link className="mr-4" href="/about">About</Link>
              <Link href="/blog">Blog</Link>
            </nav>
          </header>
          <Animate>
            <main>
              {children}
            </main>
          </Animate>
          <footer></footer>
        </Transitions>
      </body>
    </html>
  );
}

But we’re not done yet.

We have to create a component that replaces the native NextLink.

// Link.tsx

"use client";

import NextLink from "next/link";
import { ComponentProps } from "react";
import { useNavigationTransition } from "./context";
import { usePathname } from "next/navigation";

type Props = ComponentProps<typeof NextLink>;


const Link = (props: Props) => {
  const routePath = usePathname();
  const { navigate } = useNavigationTransition();
  return (
    <NextLink
      {...props}
      onClick={(e) => {
        e.preventDefault();
        const href = e.currentTarget.getAttribute("href");
        if (href === routePath) return;
        if (href) navigate(href);
      }}
    />
  );
};

export default Link;

You can see the complete project on GitHub and Codesandbox:

If you found it useful, share the article.

Juan Núñez Blaco

Full Stack Developer