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.