Animations make websites feel alive and professional. Framer Motion provides a declarative API for creating animations in React. Here are the most useful patterns.
Installation
pnpm add motion
Basic Entrance Animation
"use client";
import { motion } from "motion/react";
export function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
initial: Starting state (invisible, shifted down 20px)animate: End state (visible, original position)transition: How the animation plays (0.5 seconds)
Scroll-Triggered Animations
Animate elements when they enter the viewport:
"use client";
import { motion } from "motion/react";
export function ScrollReveal({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
whileInView: Triggers when element enters viewportviewport.once: Only animates once (does not replay on scroll back)viewport.margin: Triggers 100px before entering viewport
Staggered Children
Animate a list of items one after another:
"use client";
import { motion } from "motion/react";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
export function StaggeredList({ items }: { items: string[] }) {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((text, i) => (
<motion.li key={i} variants={item}>
{text}
</motion.li>
))}
</motion.ul>
);
}
Each child animates 0.1 seconds after the previous one.
Hover and Tap Animations
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="rounded-md bg-blue-600 px-6 py-3 text-white"
>
Click Me
</motion.button>
Subtle scale changes give buttons a tactile feel.
Page Transitions
Wrap your page content with AnimatePresence:
// 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 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
Using template.tsx in Next.js App Router re-renders on every navigation, triggering the entrance animation.
Animated Counter
"use client";
import { useInView, useMotionValue, useTransform, animate } from "motion/react";
import { useEffect, useRef } from "react";
export function AnimatedCounter({ target, duration = 2 }: { target: number; duration?: number }) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const count = useMotionValue(0);
useEffect(() => {
if (isInView) {
animate(count, target, { duration });
}
}, [isInView, count, target, duration]);
useEffect(() => {
const unsubscribe = count.on("change", (latest) => {
if (ref.current) {
ref.current.textContent = Math.round(latest).toLocaleString();
}
});
return unsubscribe;
}, [count]);
return <span ref={ref}>0</span>;
}
Usage:
<p>
<AnimatedCounter target={500} /> projects delivered
</p>
Accordion Animation
"use client";
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border-b">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between py-4 text-left font-medium"
>
{title}
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
βΌ
</motion.span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="pb-4">{children}</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Performance Tips
- Animate transform and opacity only: These properties are GPU-accelerated
- Use
layoutprop sparingly: Layout animations can be expensive - Set
once: trueon scroll animations: Prevents re-animation - Avoid animating height directly: Use
scaleYinstead when possible - Use
will-changecautiously: Framer Motion handles this internally
Reduced Motion Support
Respect user preferences:
import { useReducedMotion } from "motion/react";
export function AnimatedCard({ children }: { children: React.ReactNode }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
>
{children}
</motion.div>
);
}
When the user has "Reduce Motion" enabled in their OS settings, animations are disabled.
Common Mistake: Client Component Boundary
Framer Motion components must be client components. Mark them with "use client". Import them into Server Components without issues:
// Server Component (no "use client")
import { FadeIn } from "@/components/FadeIn"; // Client component
export default function Page() {
return (
<FadeIn>
<h1>This works fine</h1>
</FadeIn>
);
}
Need Animated Design?
We create engaging, animated websites that perform well across all devices. Contact us for professional web design with tasteful animations.