Aug 2026 · Guide · ~10 min read
Framer Motion Complete Guide — Animations That Don't Kill Performance
By Safdar Ali — frontend engineer, Bengaluru
I'm Safdar Ali, a frontend engineer in Bengaluru. Framer Motion is the library I reach for when motion should feel intentional — page transitions, staggered lists, micro-interactions on safdarali.in. It is also the library I've seen wreck Lighthouse scores when teams animate everything at once. This framer motion tutorial 2026 is not "add motion.div to every element." It is how to stay at 60fps on a ₹15,000 Android phone while still shipping polish.
I learned this the hard way on a portfolio template that animated every section entrance with layout enabled on the wrapper. Lighthouse performance dropped eight points. Users did not compliment the motion — they left before the hero video loaded. Motion should guide attention, not compete with content. The patterns below are what I use after that postmortem and after shipping four marketing sites in 2025–2026 with Framer Motion plus Next.js App Router.
If you are searching framer motion tutorial 2026, you probably want copy-paste patterns that survive production, not a CodeSandbox with twelve animated emojis. This guide assumes React 19 and Next.js 15. Motion components stay in client leaves; everything else remains a Server Component. That split is non-negotiable for performance — same rule as my RSC articles.
Why animation costs real milliseconds on the main thread
Every animated property triggers style recalculation, layout, and paint. Animating width, top, or margin forces layout. Animating transform and opacity stays on the compositor — that is the 60fps rule in one sentence.
// BEFORE — layout-thrashing hover (avoid)
<motion.div
whileHover={{ width: "120%", marginTop: 8 }}
/>
// AFTER — compositor-friendly hover
<motion.div
whileHover={{ scale: 1.02, opacity: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>Framer Motion defaults are good, but it will happily animate whatever you pass. Your job is to pass the right properties. On a client marketing rebuild, switching width animations to scale dropped main-thread work during scroll by a visible margin in Chrome Performance — same lesson as my Next.js performance case study.
Developers in Bengaluru often test on MacBooks and assume smooth scrolling equals done. Ship to a ₹12,000 Android phone before you call animation finished. Chrome Remote Debugging to a physical device beats guessing from desktop emulation. If frames drop, open Performance panel, look for purple Layout blocks synchronized with your motion components — that is your smoking gun.
Will-change CSS can help compositor promotion but is not a license to animate everything. I set will-change sparingly on heroes that animate once, then remove it after animation complete via onAnimationComplete. Framer Motion exposes lifecycle callbacks — use them to avoid leaving expensive hints on hundreds of nodes.
Lazy-load Framer Motion so the first paint stays fast
The full motion bundle is not free. On content-heavy pages, I load motion only inside client islands that need it — never in the root layout of a Server Component tree.
// components/FadeInSection.tsx — client leaf only
"use client";
import { lazy, Suspense } from "react";
const MotionDiv = lazy(() =>
import("framer-motion").then((mod) => ({
default: mod.motion.div,
}))
);
export function FadeInSection({ children }: { children: React.ReactNode }) {
return (
<Suspense fallback={<div className="opacity-0">{children}</div>}>
<MotionDiv
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
>
{children}
</MotionDiv>
</Suspense>
);
}Pair this with dynamic import at the route level for hero sections that do not need motion on first paint. Users on 4G in tier-2 cities get HTML and CSS first; motion hydrates when the chunk arrives.
Bundle analyzer tip: compare your main layout chunk before and after importing framer-motion globally. I have seen 35KB gzip added to the entry path when motion landed in providers.tsx by mistake. Move it to the components that need springs, not the root. Pair with dynamic import for below-fold sections so First Contentful Paint does not wait on animation code.
Alternative: CSS @keyframes for simple fades cost zero JavaScript. I still use Framer when gestures (drag, layoutId) matter. The decision tree is: can CSS alone do it in under ten lines? If yes, skip motion. If you need orchestration across children, use variants. If you need shared element transitions, use layoutId with a small node count.
Layout animations — powerful, expensive, use sparingly
layout and layoutId are Framer Motion's superpower for shared-element transitions. They also measure the DOM every frame. Use them on small lists, not on 200-card grids.
// app/projects/ProjectGrid.tsx — layout on cards, not the grid wrapper
"use client";
import { motion } from "framer-motion";
export function ProjectCard({ id, title }: { id: string; title: string }) {
return (
<motion.article
layout
layoutId={id}
className="rounded-xl border p-4"
transition={{ layout: { duration: 0.2 } }}
>
{title}
</motion.article>
);
}// BEFORE — layout on parent + every child = layout storm
<motion.ul layout>
{items.map((item) => (
<motion.li key={item.id} layout layoutId={item.id}>
{item.label}
</motion.li>
))}
</motion.ul>
// AFTER — layout only on the item being reordered (filter/sort UI)
<motion.ul>
{items.map((item) => (
<motion.li key={item.id} layout={activeId === item.id}>
{item.label}
</motion.li>
))}
</motion.ul>layoutId shared transitions between routes look magical on Dribbble shots. In production they require both pages mounted with AnimatePresence or a layout route wrapper — more client JavaScript. I use them on project detail modals, not on blog indexes with fifty cards. Measure before you replicate award-winning UI on data-heavy pages.
Respect prefers-reduced-motion — it is not optional
WCAG expects you to honour system settings. Framer Motion does not do this automatically. Wrap variants or swap durations when prefers-reduced-motion: reduce is set — I align this with my WCAG 2.2 React guide.
"use client";
import { motion, useReducedMotion } from "framer-motion";
export function HeroTitle({ text }: { text: string }) {
const reduce = useReducedMotion();
return (
<motion.h1
initial={reduce ? false : { opacity: 0, y: 24 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
transition={reduce ? { duration: 0 } : { duration: 0.5 }}
>
{text}
</motion.h1>
);
}Test with macOS System Settings → Accessibility → Display → Reduce motion, and on Android Developer options. Your Bengaluru users include people who enable this permanently.
Also respect battery saver modes — some Android OEMs throttle animations globally. Your site should remain usable with zero motion. That means content is never opacity-zero waiting for whileInView without a reduced-motion fallback. I set initial=false when reduced motion is detected so server-rendered text is immediately readable.
Staggered lists without blocking scroll
Stagger children look premium on portfolio sections. Keep stagger count under ~12 visible items and use viewport once so off-screen items do not animate on every scroll pass.
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.06, delayChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 16 },
show: { opacity: 1, y: 0 },
};
// motion.ul + motion.li variants — only when section enters view
<motion.ul variants={container} initial="hidden" whileInView="show" viewport={{ once: true }>
{skills.map((s) => (
<motion.li key={s.id} variants={item}>
{s.name}
</motion.li>
))}
</motion.ul>Animation approaches — performance comparison
| Approach | Typical FPS (mid Android) | Bundle impact | Best for |
|---|---|---|---|
| CSS transitions (transform/opacity) | 60 | 0 KB JS | Hover, simple fades |
| Framer Motion (compositor props) | 55–60 | ~30 KB gzip (lazy) | Gestures, springs, orchestration |
| Framer layout animations | 40–55 on long lists | Same bundle | Reorder, shared elements (small sets) |
| Animating width/height/top | 30–45 | N/A | Avoid in production |
| Lottie / Rive full-screen | Varies | 50–200+ KB | Hero illustrations, not nav |
| React Spring | 55–60 | ~25 KB gzip | Physics-heavy UIs |
Takeaway: Framer Motion wins when you need coordinated variants and layoutId. CSS wins when you need one hover state and zero JS.
Spring physics feel premium but cost more calculation than linear easing. For list staggers I use tween with duration 0.25s — users perceive polish without bouncy overshoot on every line item. Reserve springs for primary CTAs and modal entrances where brand personality matters.
Framer Motion inside Next.js App Router — boundaries that work
Motion components are always client components. Keep them as leaves under Server Component pages. Do not mark entire pages "use client" just for a fade-in.
// app/blog/page.tsx — Server Component shell
import { BlogList } from "@/components/BlogList";
import { PageTransition } from "@/components/PageTransition";
export default function BlogPage() {
return (
<PageTransition>
<BlogList />
</PageTransition>
);
}
// components/PageTransition.tsx
"use client";
import { motion } from "framer-motion";
export function PageTransition({ children }: { children: React.ReactNode }) {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
{children}
</motion.div>
);
}For route transitions with exit animations, you need AnimatePresence and a client layout wrapper — acceptable for marketing sites, rarely worth it for dashboards. Read RSC vs client components before wrapping your root layout.
Route transition example: wrap only the segment that changes in template.tsx client component, not the entire app shell with sidebar and footer. Sidebars should not re-animate on every navigation — users notice repetitive motion and it reads as slow software. Subtle crossfade on main content pane is enough.
will-change and transform3d hints can help GPU compositing but overuse increases memory on low-RAM Android phones common in India. Apply will-change only during active animation via motion props, not statically on every card in a grid. If Chrome Performance shows excessive layers, remove hints and reduce simultaneous animating elements. Performance and accessibility intersect when animations cause nausea — prefers-reduced-motion is non-negotiable on hero and modal entrances.
Debugging jank — Chrome Performance and the 60fps checklist
Record a Performance profile while scrolling your animated page. If you see long Layout tasks tied to motion, you are animating the wrong properties or running layout on too many nodes.
// Production checklist (paste in PR description)
// [ ] Only transform + opacity animated on scroll paths
// [ ] layout / layoutId on < 20 nodes
// [ ] useReducedMotion() on hero and modals
// [ ] motion lazy-loaded or in client leaves
// [ ] whileInView viewport once: true
// [ ] No animation on LCP image or H1 until after first paintI run this before every launch that uses motion. One portfolio section with careless layout animations cost me 8 Lighthouse performance points until I scoped layout to the active card only.
Document animation budgets in PR descriptions: which pages animate, max simultaneous layout nodes, reduced-motion test screenshot. Future you — and teammates — will not guess why motion was added. I keep a short MOTION.md in repos with more than three animated routes.
Interview tip: if someone asks Framer Motion vs GSAP, answer with use case. GSAP excels at timeline-heavy storytelling; Framer excels at React state-driven UI transitions. Many teams use both — GSAP for hero sequences, Framer for component interactions. Do not cargo-cult one library because Twitter prefers it this month.
Bundle size note: framer-motion is tree-shakeable but still kilobytes you pay on every client island. Compare against CSS @keyframes for simple fades — zero JavaScript cost. Use Motion only when interaction state drives the animation — drag, layout reorder, staggered list exit. Marketing sites I build in Bengaluru often need three motion components total, not thirty. Audit import paths: import from framer-motion/dist/es/render/dom/motion-minimal patterns if you need extreme diet bundles, but lazy dynamic import of motion components is usually enough.
Stagger children animations with variants and transition staggerChildren — delightful in marketing hero sections, expensive when twenty list items each animate on scroll. Cap stagger to the first six items or use once: true viewport so users scrolling a long jobs page in Bengaluru do not trigger hundreds of compositor layers. Measure FPS in Chrome rendering tab if you are unsure; below 50fps on mid-range Android, simplify.
useScroll and useTransform are powerful for parallax heroes — and expensive when attached to window scroll on every pixel. Prefer CSS scroll-driven animations where browser support allows, or cap parallax to desktop via matchMedia. Mobile users on Jio in Bengaluru need stable scroll, not jittery translateY tied to scroll events firing sixty times per second.
The single takeaway
Framer Motion in 2026 is a precision tool, not glitter. Lazy-load it, animate transform and opacity, honour reduced motion, and treat layout animations like a expensive API. Your users in India are on mid-range phones and metered data — 60fps is respect, not aesthetics.
Start small: one fade-in section, measure LCP, add a second animation only if metrics allow. Motion is seasoning — the dish is still your content, accessibility, and load time. Ship, measure on real devices, iterate. The framer motion tutorial 2026 lesson is restraint: one library, few animations, measurable FPS, and respect for users who disable motion. That ships faster than parallax on every section and wins trust with performance-conscious clients in India.
AnimatePresence mode="wait" prevents overlapping exit and enter animations on route changes — cleaner on marketing sites, slower perceived navigation if overused. mode="sync" allows overlap; pick based on whether you prioritize clarity or speed. Document the choice in MOTION.md so the next contributor does not flip modes without testing LCP on 4G.
framer motion tutorial 2026 summary: lazy-load the library, animate transform and opacity, respect prefers-reduced-motion, cap layout animations, and measure on mid-range Android before client demo day. Motion should never be the reason your Lighthouse performance score stays in the orange — content and images matter more, but bad animation tips the scale.
Related: How I cut load time by 60% with Next.js, safdarali.in/projects, contact.
If this helped you
I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:
- Buy me a coffee at buymeacoffee.com/safdarali
- Subscribe to my YouTube channel — it's free; 70+ React & Next.js tutorials
Related reading
More guides on safdarali.in — same author, production-focused.
- Guide
How to Build a Frontend Developer Portfolio That Stands Out
Frontend developer portfolio guide for India — sections, React/Next.js examples, SEO, performance, personal branding, FAQ, and checklist to build and rank.
May 2026Read article →
- Guide
React Server Components vs Client Components — When to Use Which
Practical RSC vs client guide for Next.js App Router — when to use each, real code, bundle before/after, and performance impact.
May 2026Read article →