Skip to main content
Back to Blog
Tutorials
2 min read
November 19, 2024

How to Create Animated Page Transitions in Next.js

Add smooth page transitions to your Next.js app using Framer Motion's AnimatePresence and layout animations.

Ryel Banfield

Founder & Lead Developer

Smooth page transitions make your app feel polished and native. Here is how to implement them with Framer Motion.

Step 1: Install Framer Motion

pnpm add motion

Step 2: Template-Based Transitions (App Router)

The App Router's template.tsx remounts on navigation, making it ideal for page transitions.

// app/template.tsx
"use client";

import { motion } from "motion/react";

export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ ease: "easeOut", duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

Step 3: Fade Transition Variant

"use client";

import { motion } from "motion/react";

const fadeVariants = {
  hidden: { opacity: 0 },
  enter: { opacity: 1 },
  exit: { opacity: 0 },
};

export function FadeTransition({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={fadeVariants}
      initial="hidden"
      animate="enter"
      exit="exit"
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

Step 4: Slide Transitions

"use client";

import { motion } from "motion/react";

// Slide from right
export function SlideRight({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ x: 100, opacity: 0 }}
      animate={{ x: 0, opacity: 1 }}
      transition={{ type: "spring", stiffness: 260, damping: 20 }}
    >
      {children}
    </motion.div>
  );
}

// Slide up
export function SlideUp({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ y: 40, opacity: 0 }}
      animate={{ y: 0, opacity: 1 }}
      transition={{ ease: [0.25, 0.1, 0.25, 1], duration: 0.4 }}
    >
      {children}
    </motion.div>
  );
}

// Scale in
export function ScaleIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ scale: 0.95, opacity: 0 }}
      animate={{ scale: 1, opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
}

Step 5: Staggered Content Animation

"use client";

import { motion } from "motion/react";

const containerVariants = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
};

export function StaggeredPage({ children }: { children: React.ReactNode }) {
  return (
    <motion.div variants={containerVariants} initial="hidden" animate="show">
      {children}
    </motion.div>
  );
}

export function StaggeredItem({ children }: { children: React.ReactNode }) {
  return <motion.div variants={itemVariants}>{children}</motion.div>;
}

// Usage
<StaggeredPage>
  <StaggeredItem>
    <h1>Page Title</h1>
  </StaggeredItem>
  <StaggeredItem>
    <p>First paragraph</p>
  </StaggeredItem>
  <StaggeredItem>
    <div>Content section</div>
  </StaggeredItem>
</StaggeredPage>

Step 6: Scroll-Triggered Animations

"use client";

import { motion, useInView } from "motion/react";
import { useRef } from "react";

export function ScrollReveal({
  children,
  className,
}: {
  children: React.ReactNode;
  className?: string;
}) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: "-100px" });

  return (
    <motion.div
      ref={ref}
      className={className}
      initial={{ opacity: 0, y: 50 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      {children}
    </motion.div>
  );
}

// Usage
<ScrollReveal>
  <section className="py-20">
    <h2>This animates on scroll</h2>
  </section>
</ScrollReveal>

Step 7: Loading Bar Transition

"use client";

import { useEffect, useState } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "motion/react";

export function NavigationProgress() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const [isNavigating, setIsNavigating] = useState(false);

  useEffect(() => {
    setIsNavigating(true);
    const timeout = setTimeout(() => setIsNavigating(false), 500);
    return () => clearTimeout(timeout);
  }, [pathname, searchParams]);

  return (
    <AnimatePresence>
      {isNavigating && (
        <motion.div
          className="fixed left-0 top-0 z-[9999] h-0.5 bg-blue-600"
          initial={{ width: "0%" }}
          animate={{ width: "90%" }}
          exit={{ width: "100%", opacity: 0 }}
          transition={{ duration: 0.5 }}
        />
      )}
    </AnimatePresence>
  );
}

Step 8: Performance Tips

// Use will-change for hardware acceleration
<motion.div
  style={{ willChange: "transform, opacity" }}
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
>

// Prefer transform properties (x, y, scale, rotate) over
// layout-triggering properties (width, height, top, left)

// Use layout animations only when needed
<motion.div layout layoutId="shared-element">

// Reduce motion for accessibility
import { useReducedMotion } from "motion/react";

function AnimatedComponent() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    />
  );
}

Need Animated Web Experiences?

We build web applications with fluid animations, interactive transitions, and polished user experiences. Contact us to discuss your project.

animationspage transitionsFramer MotionNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles