Transiciones de página y animaciones con Next.js + Framer [REPO]

El objetivo de este tutorial es implementar animaciones de navegación en Next.js. El resultado final será esta sutil animación que mejorará la apariencia de tu aplicación.

He creado un codesandbox para que puedas analizar el resultado en el siguiente enlace:

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

Ahora vamos a analizar paso a paso cómo realizar la implementación.

Por qué incluir animaciones ente rutas de tu aplicación de Next.js

En términos de experiencia de usuario las animaciones un elemento esencial que proporciona feedback cuando se realiza una acción. Utilizándose para dar la sensación de mayor interactividad y proporcionar información al cliente durante la carga de recursos.

Además, las animaciones se pueden utilizar para incrementar la sensación de velocidad de carga cuando los tiempos de carga son elevados.

Pero en muchos casos se cae en la trampa de realizar animaciones que únicamente buscan el deleite visual y demostrar la destreza del developer que hay detrás.

El que no lo haya hecho que tire la primera piedra.

Vamos a ver cómo implementar transiciones de página y animaciones básicas con Next.js y framer.

Configuración base del proyecto

Para este tutorial utilizaré la instalación base de Next.js con Typescript y 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

Todas las rutas compartirán un mismo 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>
  );
}

Una vez instalado el proyecto crearemos las siguientes rutas para realizar la implementación:

  • Home
  • About
  • Blog

Todas ellas tendrán una estructura similar.

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>
  );
}

Implementar Framer Motion para Next.js

En primer lugar importamos la librería Framer cómo indican en su web:

pnpm add framer-motion

Para implementar esta librería tenemos que crear varios componentes.

En primer lugar tenemos que crear un context provider para gestionar el estado de nuestra aplicación entre transiciones.

// 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>
  );
}

La función sleep nos servirá para determinar la duración de la transición. Que la fijaremos a través de la constante DELAY.

Después creamos un context con dos variables:

  • Pending: establece cuando la animación se está ejecutando.
  • Navigate: una función dónde manejamos el router de Next.js

Para implementar Framer tenemos que crear un componente dónde importamos desde Framer AnimatePresence y motion.

// 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>
  );
}

Ahora incluimos nuestro componente en el Layout que te mostré al inicio del artículo:

// 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>
  );
}

Pero todavía no hemos terminado.

Tenemos que crear un componente que sustituya al nativo 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;

Puedes ver el proyecto completo en GitHub y en Codesandbox:

Si te ha parecido útil comparte el artículo.

Juan Núñez Blaco

Full Stack Developer