Introduction
In 2026, animations are no longer a luxury but a standard for engaging, high-performance interfaces. Framer Motion, Framer's official React library, shines with simple declarative syntax, powerful APIs, and native GPU acceleration—outpacing CSS transitions or GSAP in React integration. Unlike generic libs, it natively handles gestures, scroll, and layout shifts without boilerplate.
This intermediate tutorial guides you through animating a Next.js app: from basics to scroll-linked effects, with 6 coded steps. You'll create fluid micro-interactions that boost user retention by 20-30% (per Nielsen studies). By the end, your app will rival Figma/Webflow benchmarks. Ready to turn static components into living experiences? (128 words)
Prerequisites
- Node.js 20+ installed
- Solid React 18+ and TypeScript fundamentals
- Familiarity with Next.js 14 (App Router)
- VS Code editor with Tailwind CSS IntelliSense extension
- CSS/Flexbox knowledge for positioning
Set Up the Next.js Project
npx create-next-app@latest framer-motion-demo --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd framer-motion-demo
npm install framer-motion
npm run devThis command creates a Next.js 14 project ready for TypeScript and Tailwind, then installs Framer Motion (v11+ in 2026). It sets up the App Router for server-side pages and import aliases. Run npm run dev for a hot-reload server at http://localhost:3000. Skip legacy templates to leverage RSC optimizations.
First Animated Render
Replace the default content in src/app/page.tsx to test Framer Motion. We use motion.div as a universal wrapper: it replaces div and adds animation props like initial, animate, and transition. Think of it as a supercharged div with real physics (springs, easing).
Basic Animated Component
import { motion } from 'framer-motion';
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24 bg-gradient-to-br from-blue-400 to-purple-600">
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="text-4xl font-bold text-white mb-8"
>
Welcome to Framer Motion!
</motion.div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-8 py-4 bg-white text-blue-600 rounded-lg font-semibold shadow-lg"
>
Click me
</motion.button>
</main>
);
}This code swaps the homepage with a fade-in/slide-up entrance and a button with hover/tap scaling. initial sets the starting state, animate the target; whileHover handles interactions. Copy-paste ready: it's self-contained and optimizes re-renders with useReducedMotion. Pitfall: skipping transition leads to abrupt defaults.
Mastering Variants
Variants centralize multiple states in an object: perfect for orchestrating groups of elements. Analogy: a conductor for your animations, avoiding prop duplication. Use animate={state} to switch dynamically.
Animation with Variants
import { motion, useMotionValue, useSpring } from 'framer-motion';
import { useState } from 'react';
type ItemProps = { title: string; index: number };
const Item = ({ title, index }: ItemProps) => (
<motion.li
variants={{
hidden: { opacity: 0, x: -50 },
visible: (i = 1) => ({
opacity: 1,
x: 0,
transition: { delay: i * 0.1 }
})
}}
initial="hidden"
animate="visible"
custom={index}
className="p-4 bg-white rounded shadow-md mb-2"
>
{title}
</motion.li>
);
export default function AnimatedList() {
const [items] = useState(['Item 1', 'Item 2', 'Item 3']);
return (
<motion.ul
initial="hidden"
whileInView="visible"
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 }
}}
className="w-80"
>
{items.map((item, index) => (
<Item key={item} title={item} index={index} />
))}
</motion.ul>
);
}This animated list uses variants for staggering (index-based delays via custom). whileInView triggers on viewport scroll. Import into page.tsx as . Benefit: children inherit parent variants. Pitfall: no custom means no stagger; test on mobile for useReducedMotion.
Customizing Transitions
Transitions control the 'how': duration, ease, type ('spring' for natural bounce). In 2026, prioritize springs for premium UX (more organic than cubic-bezier).
Advanced Transitions with Springs
import { motion } from 'framer-motion';
export default function SpringCard() {
return (
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 20,
mass: 1
}}
whileHover={{
scale: 1.1,
rotate: 5,
transition: { type: 'spring', stiffness: 400 }
}}
className="w-64 h-64 bg-gradient-to-r from-green-400 to-blue-500 rounded-xl shadow-2xl flex items-center justify-center text-white font-bold text-xl cursor-pointer"
>
Spring Magic
</motion.div>
);
}Springs mimic real physics: stiffness (rigidity), damping (friction). Hover overrides the global transition. Copy into page.tsx. Result: buttery-smooth 60fps bounce. Pitfall: extreme values cause overshoot; tweak in the Framer Motion playground.
Adding Interactive Gestures
whileDrag and drag enable native drag & drop with multi-touch. Ideal for cards or sliders. Pair with useMotionValue for custom value syncing.
Draggable Component
import { motion, useMotionValue } from 'framer-motion';
export default function DragCard() {
const x = useMotionValue(0);
const y = useMotionValue(0);
return (
<motion.div
drag
dragConstraints={{ top: -50, left: -50, right: 50, bottom: 50 }}
dragElastic={0.2}
style={{ x, y }}
whileDrag={{ scale: 1.2, rotate: 10 }}
className="w-48 h-48 bg-purple-500 rounded-lg shadow-lg flex items-center justify-center text-white font-bold cursor-grab active:cursor-grabbing"
>
Drag me
</motion.div>
);
}drag enables dragging; constraints limits the area. useMotionValue tracks position for syncing (e.g., parallax). dragElastic adds bounce. Test on touch/mobile. Pitfall: no style={{x,y}} means no sync; performant even on low-end devices.
Scroll-Linked Animations
useScroll + useTransform ties animations to viewport progress. Examples: parallax, progress bars. Use motionValue for fine control.
Scroll Parallax Effect
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
export default function ScrollParallax() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start']
});
const y = useTransform(scrollYProgress, [0, 1], ['0%', '-30%']);
return (
<section ref={ref} className="h-screen flex items-center justify-center bg-gradient-to-b from-indigo-500 to-violet-600 relative overflow-hidden">
<motion.div
style={{ y }}
className="text-6xl font-black text-white drop-shadow-2xl"
>
Parallax
</motion.div>
<motion.div
style={{ opacity: useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]) }}
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 text-white text-lg"
>
Scroll for magic
</motion.div>
</section>
);
}useScroll tracks progress; useTransform maps it to props (y/opacity). offset sets triggers. Add to a tall page. Result: fluid parallax background. Pitfall: target ref required; auto-throttled for performance.
Best Practices
- Lazy-load: Use
Suspense+whileInViewfor off-screen animations. - Performance:
transformTemplatefor GPU acceleration; limit to 3-5 motions per frame. - Accessibility: Integrate
useReducedMotionviamotionValue. - Orchestration:
layoutIdfor shared layout transitions. - Testing: Snapshots with
@testing-library/react+ mock MotionValues.
Common Pitfalls to Avoid
- Infinite re-renders: Avoid
animateinuseEffectwithout deps; useuseAnimation. - Layout thrashing: Don't mix
position: relativewith scales; prioritizetransform. - Mobile lag: Set
dragMomentum= false for crisp stops. - SSR mismatch:
initial={false}for client-only; useAnimatePresencefor exits.
Next Steps
- Official docs: Framer Motion
- Advanced examples: CodeSandbox Framer
- Video tutorials: YouTube "Framer Motion 2026 updates"
- Pro training: Learni Dev Courses on Advanced React Animations.