Skip to main content
Back to Blog
Tutorials
2 min read
December 31, 2024

How to Build Animated Page Transitions with Framer Motion in Next.js

Add smooth page transitions to your Next.js app using Framer Motion with fade, slide, and shared layout animations.

Ryel Banfield

Founder & Lead Developer

Page transitions make navigation feel polished. Here is how to implement them with Framer Motion in Next.js App Router.

Install

pnpm add motion

Template-Based Transitions

The Next.js template.tsx file re-mounts on navigation, making it perfect for page transitions.

// app/(site)/template.tsx
"use client";

import { motion } from "motion/react";

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

Fade Transition

"use client";

import { motion } from "motion/react";

export function FadeTransition({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.25 }}
    >
      {children}
    </motion.div>
  );
}

Slide Transition

"use client";

import { motion } from "motion/react";

type Direction = "left" | "right" | "up" | "down";

const variants = {
  left: { initial: { x: "100%" }, animate: { x: 0 }, exit: { x: "-100%" } },
  right: { initial: { x: "-100%" }, animate: { x: 0 }, exit: { x: "100%" } },
  up: { initial: { y: "100%" }, animate: { y: 0 }, exit: { y: "-100%" } },
  down: { initial: { y: "-100%" }, animate: { y: 0 }, exit: { y: "100%" } },
};

export function SlideTransition({
  children,
  direction = "left",
}: {
  children: React.ReactNode;
  direction?: Direction;
}) {
  const v = variants[direction];

  return (
    <motion.div
      initial={v.initial}
      animate={v.animate}
      exit={v.exit}
      transition={{ type: "spring", stiffness: 300, damping: 30 }}
    >
      {children}
    </motion.div>
  );
}

Staggered Content Animation

"use client";

import { motion } from "motion/react";

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.08,
      delayChildren: 0.1,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: "easeOut" },
  },
};

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

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

// Usage
function BlogPage() {
  return (
    <StaggeredPage>
      <StaggeredItem>
        <h1>Blog</h1>
      </StaggeredItem>
      <StaggeredItem>
        <p>Latest articles</p>
      </StaggeredItem>
      {posts.map((post) => (
        <StaggeredItem key={post.slug}>
          <PostCard post={post} />
        </StaggeredItem>
      ))}
    </StaggeredPage>
  );
}

Shared Layout Animations

"use client";

import { motion, AnimatePresence, LayoutGroup } from "motion/react";
import { useState } from "react";

interface Tab {
  id: string;
  label: string;
  content: React.ReactNode;
}

export function AnimatedTabs({ tabs }: { tabs: Tab[] }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id);

  return (
    <LayoutGroup>
      <div className="border-b flex gap-1">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            className="relative px-4 py-2 text-sm font-medium"
          >
            {tab.label}
            {activeTab === tab.id && (
              <motion.div
                layoutId="active-tab"
                className="absolute inset-x-0 bottom-0 h-0.5 bg-primary"
                transition={{ type: "spring", stiffness: 500, damping: 35 }}
              />
            )}
          </button>
        ))}
      </div>

      <AnimatePresence mode="wait">
        <motion.div
          key={activeTab}
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -8 }}
          transition={{ duration: 0.2 }}
          className="py-4"
        >
          {tabs.find((t) => t.id === activeTab)?.content}
        </motion.div>
      </AnimatePresence>
    </LayoutGroup>
  );
}

Scroll-Triggered Animations

"use client";

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

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

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

Route Transition with Progress Bar

"use client";

import { motion, useScroll } from "motion/react";

export function ScrollProgress() {
  const { scrollYProgress } = useScroll();

  return (
    <motion.div
      className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
      style={{ scaleX: scrollYProgress }}
    />
  );
}

Need Polished Web Animations?

We create smooth, performant animations that enhance user experience without hurting performance. Contact us to add motion to your site.

animationpage transitionsFramer MotionNext.jstutorial

Ready to Start Your Project?

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

Get in Touch

Related Articles