
Framer Motion
- 943 installs
- 186 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
framer-motion is a React frontend skill that helps developers generate and review high-performance Framer Motion animation code while avoiding re-render and bundle bloat issues.
About
framer-motion is a community skill from pproenca/dot-skills that packages Framer Motion performance optimization guidance for React applications. The guide contains 42 rules across 9 categories, prioritized by impact to steer automated refactoring and code generation for motion components, gestures, layout transitions, scroll-linked effects, and SVG animations. Developers reach for framer-motion when adding animations, reviewing janky interactions, or reducing unnecessary React re-renders tied to motion state. The skill triggers on tasks involving Framer Motion APIs and is structured as a prioritized rule set rather than ad hoc tips. It suits SaaS dashboards and browser-extension UIs where smooth 60fps interactions and smaller JavaScript bundles directly affect user experience and Core Web Vitals.
- 42 performance rules across 9 priority-ordered categories
- Critical focus on bundle optimization and re-render prevention
- Specific guidance for layout transitions, scroll-linked effects, gestures and SVG animations
- Prefix-based rule system (bundle-, rerender-, anim-, layout-, etc.) for automated linting and refactoring
- Hard-gate review before committing animation-heavy React components
Framer Motion by the numbers
- 943 all-time installs (skills.sh)
- +33 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #391 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill framer-motionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 943 |
|---|---|
| repo stars | ★ 186 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you optimize Framer Motion performance in React?
Generate and review high-performance Framer Motion animation code that avoids common React re-render and bundle bloat issues.
Who is it for?
React developers shipping Framer Motion animations who need systematic performance review across gestures, layout transitions, and SVG motion.
Skip if: Teams using CSS-only animations or a non-React animation library without Framer Motion dependencies.
When should I use this skill?
The user writes, reviews, or refactors Framer Motion animations, gestures, layout transitions, scroll effects, or SVG motion in React.
What you get
Refactored Framer Motion components, reduced re-render patterns, and animation code aligned to 42 prioritized performance rules.
- Optimized motion component code
- Performance rule checklist
By the numbers
- Contains 42 Framer Motion performance rules
- Organizes rules across 9 categories
Files
Community Framer Motion Best Practices
Comprehensive performance optimization guide for Framer Motion animations in React applications. Contains 42 rules across 9 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Adding animations to React components with Framer Motion
- Optimizing bundle size for animation-heavy applications
- Preventing unnecessary re-renders during animations
- Implementing layout transitions or shared element animations
- Building scroll-linked or gesture-based interactions
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Bundle Optimization | CRITICAL | bundle- |
| 2 | Re-render Prevention | CRITICAL | rerender- |
| 3 | Animation Properties | HIGH | anim- |
| 4 | Layout Animations | HIGH | layout- |
| 5 | Scroll Animations | MEDIUM-HIGH | scroll- |
| 6 | Gesture Optimization | MEDIUM | gesture- |
| 7 | Spring & Physics | MEDIUM | spring- |
| 8 | SVG & Path Animations | LOW-MEDIUM | svg- |
| 9 | Exit Animations | LOW | exit- |
Quick Reference
1. Bundle Optimization (CRITICAL)
- `bundle-lazy-motion` - Use LazyMotion and m component instead of motion
- `bundle-dynamic-features` - Dynamically import motion features
- `bundle-dom-animation` - Use domAnimation for basic animations
- `bundle-use-animate-mini` - Use mini useAnimate for simple cases
- `bundle-strict-mode` - Enable strict mode to catch accidental imports
2. Re-render Prevention (CRITICAL)
- `rerender-motion-value` - Use useMotionValue instead of useState
- `rerender-use-transform` - Derive values with useTransform
- `rerender-stable-callbacks` - Keep animation callbacks stable
- `rerender-variants-object` - Define variants outside component
- `rerender-animate-prop` - Use stable animate values
- `rerender-motion-value-event` - Use motion value events
3. Animation Properties (HIGH)
- `anim-transform-properties` - Animate transform properties
- `anim-opacity-filter` - Prefer opacity and filter for visual effects
- `anim-hardware-acceleration` - Leverage hardware acceleration
- `anim-will-change` - Use willChange prop judiciously
- `anim-independent-transforms` - Animate transforms independently
- `anim-keyframes-array` - Use keyframe arrays for sequences
4. Layout Animations (HIGH)
- `layout-dependency` - Use layoutDependency to limit measurements
- `layout-position-size` - Use layout="position" or "size" appropriately
- `layout-group` - Group related layout animations
- `layout-id-shared` - Use layoutId for shared element transitions
- `layout-scroll` - Add layoutScroll to scrollable ancestors
5. Scroll Animations (MEDIUM-HIGH)
- `scroll-use-scroll` - Use useScroll hook for scroll-linked animations
- `scroll-use-spring-smooth` - Smooth scroll animations with useSpring
- `scroll-element-tracking` - Track specific elements entering viewport
- `scroll-offset-configuration` - Configure scroll offsets
- `scroll-container-ref` - Track scroll within specific containers
6. Gesture Optimization (MEDIUM)
- `gesture-while-props` - Use whileHover/whileTap instead of handlers
- `gesture-variants-flow` - Let gesture variants flow to children
- `gesture-drag-constraints` - Use dragConstraints ref for boundaries
- `gesture-drag-elastic` - Configure dragElastic for natural feel
- `gesture-tap-cancel` - Use onTapCancel for interrupted gestures
7. Spring & Physics (MEDIUM)
- `spring-physics-based` - Use physics-based springs for interruptibility
- `spring-damping-ratio` - Configure damping to control oscillation
- `spring-mass-inertia` - Adjust mass for heavier/lighter feel
- `spring-use-spring-hook` - Use useSpring for reactive values
8. SVG & Path Animations (LOW-MEDIUM)
- `svg-path-length` - Use pathLength for line drawing animations
- `svg-motion-components` - Use motion.path and motion.circle
- `svg-viewbox-animation` - Animate viewBox for zoom effects
- `svg-morph-matching-points` - Match point counts for morphing
9. Exit Animations (LOW)
- `exit-animate-presence` - Wrap conditional renders with AnimatePresence
- `exit-unique-keys` - Provide unique keys for AnimatePresence children
- `exit-mode-wait` - Use mode="wait" for sequential transitions
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Rule Title Here
Brief explanation (1-3 sentences) of WHY this matters. Focus on performance implications and the cost of the incorrect approach.
Incorrect (description of the problem/cost):
// Component showing the anti-pattern
function ExampleComponent() {
// Comment on the problematic line explaining the cost
const problematicValue = expensiveApproach();
return (
<motion.div animate={{ opacity: 1 }}>
{/* Realistic component structure */}
</motion.div>
);
}Correct (description of the benefit/solution):
// Same component with the optimized approach
function ExampleComponent() {
// Comment explaining the benefit
const optimizedValue = efficientApproach();
return (
<motion.div animate={{ opacity: 1 }}>
{/* Same structure, minimal diff from incorrect */}
</motion.div>
);
}When NOT to use this pattern:
- Exception 1: When the simpler approach is sufficient
- Exception 2: When the optimization adds unnecessary complexity
Reference: Framer Motion Documentation
{
"name": "framer-motion",
"version": "1.0.7",
"organization": "Community",
"technology": "Framer Motion",
"date": "January 2026",
"abstract": "Comprehensive performance optimization guide for Framer Motion animations in React applications, designed for AI agents and LLMs. Contains 42 rules across 9 categories, prioritized by impact from critical (bundle optimization, re-render prevention) to incremental (exit animations). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://motion.dev/docs",
"https://motion.dev/docs/react-reduce-bundle-size",
"https://motion.dev/docs/performance",
"https://motion.dev/docs/react-motion-value",
"https://motion.dev/docs/react-layout-animations",
"https://motion.dev/docs/react-scroll-animations",
"https://motion.dev/docs/react-gestures",
"https://motion.dev/docs/react-transitions"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Bundle Optimization (bundle)
Impact: CRITICAL Description: Framer Motion's default bundle is 34kb+. Using LazyMotion and the m component reduces initial payload to <5kb, dramatically improving Time to Interactive.
2. Re-render Prevention (rerender)
Impact: CRITICAL Description: Motion values update the DOM without triggering React re-renders. Using state instead of motion values causes cascading re-renders on every animation frame.
3. Animation Properties (anim)
Impact: HIGH Description: GPU-accelerated properties (transform, opacity, filter) animate at 60-120fps. Layout-triggering properties (width, height, top, left) cause expensive reflows.
4. Layout Animations (layout)
Impact: HIGH Description: The layout prop uses FLIP technique for performant CSS transform animations. Misconfigured layout animations cause unnecessary measurements and reflows.
5. Scroll Animations (scroll)
Impact: MEDIUM-HIGH Description: Hardware-accelerated scroll animations via ScrollTimeline API remain smooth during heavy JS execution. Traditional scroll handlers cause jank.
6. Gesture Optimization (gesture)
Impact: MEDIUM Description: Built-in gesture props (whileHover, whileTap) are optimized internally. Custom event handlers often break memoization and cause unnecessary re-renders.
7. Spring & Physics (spring)
Impact: MEDIUM Description: Physics-based springs feel natural and handle interruptions gracefully. Misconfigured damping/stiffness causes excessive oscillation or sluggish animations.
8. SVG & Path Animations (svg)
Impact: LOW-MEDIUM Description: SVG animations are scalable and lightweight. Complex path morphing with mismatched point counts requires expensive interpolation.
9. Exit Animations (exit)
Impact: LOW Description: AnimatePresence enables exit animations but requires proper key management. Missing keys cause animation failures or memory leaks.
Leverage Automatic Hardware Acceleration
Motion automatically hardware-accelerates transform and opacity animations, running them on the compositor thread. This means animations continue smoothly even when JavaScript is busy. Avoid patterns that force animations back to the main thread.
Incorrect (blocking main thread during animation):
import { motion } from "framer-motion";
import { useState } from "react";
function SearchResults() {
const [results, setResults] = useState([]);
const handleSearch = async () => {
// Heavy computation runs while trying to animate
const data = await fetchResults();
const processed = data.map(item => expensiveTransform(item)); // Blocks main thread
setResults(processed);
};
return (
<div>
<button onClick={handleSearch}>Search</button>
{results.map((item, i) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
style={{ backgroundColor: item.color }} // Dynamic style forces main thread
transition={{ delay: i * 0.1 }}
>
{item.name}
</motion.div>
))}
</div>
);
}Correct (keeping animations on compositor):
import { motion } from "framer-motion";
import { useState, useDeferredValue } from "react";
function SearchResults() {
const [results, setResults] = useState([]);
const deferredResults = useDeferredValue(results);
const handleSearch = async () => {
const data = await fetchResults();
setResults(data); // Set raw data, defer processing
};
return (
<div>
<button onClick={handleSearch}>Search</button>
{deferredResults.map((item, i) => (
<motion.div
key={item.id}
className={`result-card result-${item.type}`} // CSS class instead of dynamic style
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
>
{item.name}
</motion.div>
))}
</div>
);
}Hardware-accelerated by default:
x,y,zscale,scaleX,scaleYrotate,rotateX,rotateY,rotateZopacityfilter
Tips for keeping animations smooth: 1. Use CSS classes instead of dynamic inline styles 2. Defer heavy computations with useDeferredValue or requestIdleCallback 3. Use useTransition for non-urgent state updates 4. Avoid reading layout (offsetWidth, getBoundingClientRect) during animations
Reference: Framer Motion - Hardware Acceleration
Animate Transforms Independently
Motion allows animating x, y, scale, and rotate as independent values with their own timing. CSS combines these into a single transform property, forcing identical timing. Independent transforms enable richer, more natural animations.
Incorrect (combined transform with single timing):
import { motion } from "framer-motion";
function BouncingBall() {
return (
<motion.div
className="ball"
animate={{
transform: "translateY(-100px) scale(1.2) rotate(180deg)" // All same timing
}}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
);
}
function FloatingCard() {
return (
<motion.div
className="card"
style={{
transform: `translateX(${x}px) translateY(${y}px) scale(${scale})` // Manual string
}}
animate={{ x: 100, y: -50, scale: 1.1 }}
transition={{ duration: 0.3 }} // Same duration for all
/>
);
}Correct (independent transform properties):
import { motion } from "framer-motion";
function BouncingBall() {
return (
<motion.div
className="ball"
animate={{
y: -100,
scale: 1.2,
rotate: 180
}}
transition={{
y: { type: "spring", stiffness: 300, damping: 10 }, // Bouncy vertical
scale: { duration: 0.3, ease: "easeOut" }, // Quick scale
rotate: { duration: 0.8, ease: "linear" } // Slow spin
}}
/>
);
}
function FloatingCard() {
return (
<motion.div
className="card"
animate={{ x: 100, y: -50, scale: 1.1 }}
transition={{
x: { type: "spring", stiffness: 100 }, // Springy horizontal
y: { type: "spring", stiffness: 200 }, // Stiffer vertical
scale: { delay: 0.1, duration: 0.2 } // Delayed scale pop
}}
/>
);
}Available independent transforms:
x,y,z- translationscale,scaleX,scaleY- scalingrotate,rotateX,rotateY,rotateZ- rotationskew,skewX,skewY- skewing
Practical example - card flip with stagger:
function FlipCard({ isFlipped }) {
return (
<motion.div
className="card"
animate={{
rotateY: isFlipped ? 180 : 0,
scale: isFlipped ? 1.1 : 1,
y: isFlipped ? -20 : 0
}}
transition={{
rotateY: { duration: 0.6, ease: [0.4, 0, 0.2, 1] }, // Smooth flip
scale: { duration: 0.3, delay: 0.15 }, // Scale at midpoint
y: { type: "spring", stiffness: 200, delay: 0.1 } // Lift with spring
}}
/>
);
}Reference: Framer Motion - Transitions
Use Keyframe Arrays for Complex Sequences
Keyframe arrays define multi-step animations in a single declaration, allowing Motion to interpolate smoothly between values. Chaining multiple animate calls or using useEffect for sequences causes unnecessary re-renders and timing issues.
Incorrect (chained animations with state):
import { motion, useAnimation } from "framer-motion";
import { useEffect } from "react";
function PulsingDot() {
const controls = useAnimation();
useEffect(() => {
const sequence = async () => {
await controls.start({ scale: 1.5 }); // First animation
await controls.start({ scale: 0.8 }); // Wait, then second
await controls.start({ scale: 1 }); // Wait, then third
sequence(); // Loop (causes re-renders)
};
sequence();
}, [controls]);
return <motion.div className="dot" animate={controls} />;
}
function ShakingButton() {
const controls = useAnimation();
const handleError = async () => {
await controls.start({ x: -10 });
await controls.start({ x: 10 });
await controls.start({ x: -10 });
await controls.start({ x: 10 });
await controls.start({ x: 0 }); // Five separate animations
};
return (
<motion.button animate={controls} onClick={handleError}>
Submit
</motion.button>
);
}Correct (keyframe arrays):
import { motion } from "framer-motion";
function PulsingDot() {
return (
<motion.div
className="dot"
animate={{
scale: [1, 1.5, 0.8, 1] // All keyframes in one array
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.3, 0.7, 1] // Control timing of each keyframe
}}
/>
);
}
function ShakingButton() {
const [isError, setIsError] = useState(false);
return (
<motion.button
animate={isError ? { x: [0, -10, 10, -10, 10, 0] } : { x: 0 }}
transition={{ duration: 0.4 }}
onAnimationComplete={() => setIsError(false)}
onClick={() => setIsError(true)}
>
Submit
</motion.button>
);
}Keyframe features:
// Multiple properties with keyframes
<motion.div
animate={{
x: [0, 100, 100, 0],
y: [0, 0, 100, 100],
rotate: [0, 90, 180, 270]
}}
transition={{
duration: 2,
times: [0, 0.25, 0.5, 0.75], // Sync timing across properties
ease: ["easeIn", "linear", "easeOut"] // Different ease per segment
}}
/>
// Starting from current value with null
<motion.div
animate={{
opacity: [null, 0.5, 1] // null = start from current value
}}
transition={{ duration: 0.5 }}
/>
// Looping with repeat
<motion.div
animate={{ rotate: [0, 360] }}
transition={{
duration: 2,
repeat: Infinity,
repeatType: "loop", // "loop" | "reverse" | "mirror"
ease: "linear"
}}
/>When to use keyframes vs variants:
- Keyframes: Single element, continuous motion, loops
- Variants + staggerChildren: Multiple elements, orchestrated sequences
Reference: Framer Motion - Keyframes
Prefer Opacity and Filter for Visual Effects
Opacity and filter are composite-only properties that run on the GPU compositor thread. Animating properties like background-color, box-shadow, or border triggers expensive paint operations on the main thread.
Incorrect (paint-triggering properties):
import { motion } from "framer-motion";
function HoverButton() {
return (
<motion.button
className="btn"
initial={{ backgroundColor: "#3b82f6" }}
whileHover={{ backgroundColor: "#1d4ed8" }} // Triggers repaint every frame
transition={{ duration: 0.2 }}
>
Click me
</motion.button>
);
}
function FadeCard() {
return (
<motion.div
className="card"
initial={{ boxShadow: "0 0 0 rgba(0,0,0,0)" }}
animate={{ boxShadow: "0 10px 40px rgba(0,0,0,0.3)" }} // Expensive shadow paint
transition={{ duration: 0.3 }}
>
<p>Card content</p>
</motion.div>
);
}Correct (composite-only properties):
import { motion } from "framer-motion";
function HoverButton() {
return (
<motion.button
className="btn btn-blue"
initial={{ opacity: 1 }}
whileHover={{ opacity: 0.8 }} // GPU-accelerated, no repaint
transition={{ duration: 0.2 }}
>
Click me
</motion.button>
);
}
function FadeCard() {
return (
<motion.div
className="card card-with-shadow"
initial={{ opacity: 0, filter: "blur(4px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }} // Both GPU-accelerated
transition={{ duration: 0.3 }}
>
<p>Card content</p>
</motion.div>
);
}GPU-accelerated properties:
opacity- visibility transitionsfilter- blur, brightness, contrast, grayscale, saturatetransform(x, y, scale, rotate)
Paint-triggering properties to avoid animating:
backgroundColor,colorboxShadow,textShadowborder,borderRadiusoutline
Tip: For color transitions, use CSS transitions on the element and control visibility with Motion's opacity.
Reference: Framer Motion - Animation
Animate Transform Properties Instead of Layout Properties
Transform properties (x, y, scale, rotate) are GPU-accelerated and bypass the browser's layout engine. Animating layout properties (width, height, top, left) triggers expensive reflows on every frame, causing jank.
Incorrect (layout-triggering properties):
import { motion } from "framer-motion";
function ExpandingCard() {
return (
<motion.div
className="card"
initial={{ width: 200, height: 100 }}
animate={{ width: 400, height: 200 }} // Triggers layout recalculation every frame
transition={{ duration: 0.5 }}
>
<p>Content here</p>
</motion.div>
);
}
function SlidingPanel() {
return (
<motion.div
className="panel"
initial={{ left: -300 }}
animate={{ left: 0 }} // Causes reflow, not GPU-accelerated
transition={{ duration: 0.3 }}
>
<nav>Menu items</nav>
</motion.div>
);
}Correct (GPU-accelerated transforms):
import { motion } from "framer-motion";
function ExpandingCard() {
return (
<motion.div
className="card"
initial={{ scale: 0.5 }}
animate={{ scale: 1 }} // GPU-accelerated, no layout recalculation
transition={{ duration: 0.5 }}
>
<p>Content here</p>
</motion.div>
);
}
function SlidingPanel() {
return (
<motion.div
className="panel"
initial={{ x: -300 }}
animate={{ x: 0 }} // GPU-accelerated transform
transition={{ duration: 0.3 }}
>
<nav>Menu items</nav>
</motion.div>
);
}Property mapping:
top/left/right/bottom→x,ywidth/height→scale,scaleX,scaleY- CSS
transform: rotate()→rotate
When layout properties are needed: Use the layout prop which uses FLIP technique to animate layout changes performantly via transforms.
Reference: Framer Motion - Animation
Use willChange Prop Judiciously
The willChange prop hints to the browser that an element will animate, allowing it to optimize ahead of time. However, overusing it creates unnecessary GPU layers, increases memory usage, and can actually hurt performance.
Incorrect (excessive willChange):
import { motion } from "framer-motion";
function CardGrid({ items }) {
return (
<div className="grid">
{items.map((item) => (
<motion.div
key={item.id}
className="card"
style={{ willChange: "transform, opacity, filter, box-shadow" }} // Too many properties
whileHover={{ scale: 1.05 }}
>
<motion.img
src={item.image}
style={{ willChange: "transform" }} // Every image gets a layer
/>
<motion.p style={{ willChange: "opacity" }}>{item.title}</motion.p> // Overkill
</motion.div>
))}
</div>
);
}Correct (targeted willChange):
import { motion } from "framer-motion";
import { useState } from "react";
function CardGrid({ items }) {
const [hoveredId, setHoveredId] = useState(null);
return (
<div className="grid">
{items.map((item) => (
<motion.div
key={item.id}
className="card"
style={{
willChange: hoveredId === item.id ? "transform" : "auto" // Only when needed
}}
onHoverStart={() => setHoveredId(item.id)}
onHoverEnd={() => setHoveredId(null)}
whileHover={{ scale: 1.05 }}
>
<img src={item.image} />
<p>{item.title}</p>
</motion.div>
))}
</div>
);
}When to use willChange:
- Complex animations that stutter without it
- Elements that animate frequently (navigation, modals)
- Large elements where layer creation cost is justified
When NOT to use willChange:
- Simple opacity/transform animations (Motion handles this)
- Static elements that rarely animate
- Many elements at once (100+ cards in a grid)
- As a "just in case" optimization
Motion's automatic optimization: Motion already promotes elements to their own compositor layer during animation. Manual willChange is rarely needed.
Alternative approach:
// Let Motion handle layer promotion automatically
<motion.div
whileHover={{ scale: 1.05 }}
transition={{ type: "spring", stiffness: 300 }}
>
{/* Motion creates GPU layer only during animation */}
</motion.div>Reference: MDN - will-change
Use domAnimation for Basic Animations
The domMax feature bundle includes layout animations, drag, and pan gestures which many apps do not need. Using domAnimation instead provides all common animation features (animate, exit, gestures like hover/tap) at 17kb less cost.
Incorrect (domMax when only basic animations needed):
// app/layout.tsx
import { LazyMotion, domMax } from "framer-motion"; // Includes layout, drag, pan
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domMax}>
{children}
</LazyMotion>
);
}
// components/NavigationMenu.tsx
import { m } from "framer-motion";
export function NavigationMenu({ items }: NavigationMenuProps) {
return (
<m.nav initial={{ y: -20 }} animate={{ y: 0 }}>
{items.map((item) => (
<m.a
key={item.href}
href={item.href}
whileHover={{ color: "#0066cc" }} // Only uses basic animations
whileTap={{ scale: 0.98 }}
>
{item.label}
</m.a>
))}
</m.nav>
);
}Correct (domAnimation for basic animations):
// app/layout.tsx
import { LazyMotion, domAnimation } from "framer-motion"; // 17kb smaller
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
);
}
// components/NavigationMenu.tsx
import { m } from "framer-motion";
export function NavigationMenu({ items }: NavigationMenuProps) {
return (
<m.nav initial={{ y: -20 }} animate={{ y: 0 }}>
{items.map((item) => (
<m.a
key={item.href}
href={item.href}
whileHover={{ color: "#0066cc" }}
whileTap={{ scale: 0.98 }}
>
{item.label}
</m.a>
))}
</m.nav>
);
}When to use domMax instead:
- You need
layoutorlayoutIdanimations - You need drag gestures (
drag,dragConstraints) - You need pan gestures (
onPan,onPanStart)
Reference: Framer Motion - Feature Bundles
Dynamically Import Motion Features
Static imports of motion features block hydration even when animations are not immediately visible. Using dynamic imports with loadFeatures defers the feature bundle until after the initial page load, improving Time to Interactive.
Incorrect (static import blocks hydration):
// app/layout.tsx
import { LazyMotion, domMax } from "framer-motion"; // domMax loaded synchronously
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domMax}>
{children}
</LazyMotion>
);
}Correct (dynamic import defers loading):
// app/layout.tsx
import { LazyMotion } from "framer-motion";
const loadFeatures = () =>
import("framer-motion").then((mod) => mod.domMax); // Loaded after hydration
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={loadFeatures}>
{children}
</LazyMotion>
);
}With custom feature bundle for maximum control:
// lib/motion-features.ts
import { domAnimation } from "framer-motion";
export default domAnimation;
// app/layout.tsx
import { LazyMotion } from "framer-motion";
const loadFeatures = () =>
import("@/lib/motion-features").then((mod) => mod.default);
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={loadFeatures}>
{children}
</LazyMotion>
);
}Reference: Framer Motion - LazyMotion
Use LazyMotion and m Component Instead of motion
The motion component includes all animation features by default, adding 34kb to your bundle. Using LazyMotion with the m component lets you load only the features you need, reducing initial payload to under 5kb.
Incorrect (full motion bundle loaded):
// components/ProductCard.tsx
import { motion } from "framer-motion"; // 34kb+ included in bundle
export function ProductCard({ product }: ProductCardProps) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
whileHover={{ scale: 1.02 }}
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
</motion.div>
);
}Correct (lazy-loaded features, minimal bundle):
// components/ProductCard.tsx
import { m } from "framer-motion"; // Only 2.5kb, features loaded separately
export function ProductCard({ product }: ProductCardProps) {
return (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
whileHover={{ scale: 1.02 }}
>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
</m.div>
);
}
// app/layout.tsx (or _app.tsx)
import { LazyMotion, domAnimation } from "framer-motion";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
);
}Reference: Framer Motion - Reduce Bundle Size
Enable Strict Mode to Catch Accidental motion Imports
When using LazyMotion, developers might accidentally import motion instead of m, silently adding the full 34kb bundle. Enabling strict mode on LazyMotion throws an error when a motion component is rendered, catching these mistakes during development.
Incorrect (no strict mode, accidental import goes unnoticed):
// app/layout.tsx
import { LazyMotion, domAnimation } from "framer-motion";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
);
}
// components/SearchInput.tsx
import { motion } from "framer-motion"; // Accidental import - no warning!
export function SearchInput({ onSearch }: SearchInputProps) {
return (
<motion.div // Silently adds 34kb to bundle
initial={{ width: 200 }}
animate={{ width: 300 }}
>
<input type="search" onChange={(e) => onSearch(e.target.value)} />
</motion.div>
);
}Correct (strict mode catches accidental imports):
// app/layout.tsx
import { LazyMotion, domAnimation } from "framer-motion";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<LazyMotion features={domAnimation} strict>
{children}
</LazyMotion>
);
}
// components/SearchInput.tsx
import { m } from "framer-motion"; // Correct import with m component
export function SearchInput({ onSearch }: SearchInputProps) {
return (
<m.div // Works correctly with LazyMotion
initial={{ width: 200 }}
animate={{ width: 300 }}
>
<input type="search" onChange={(e) => onSearch(e.target.value)} />
</m.div>
);
}Error thrown with strict mode when using motion:
Error: motion components must be wrapped in a LazyMotion component with
the "domAnimation" or "domMax" features. You are using a motion component
but strict mode is enabled.Reference: Framer Motion - LazyMotion strict
Use Mini useAnimate for Simple Cases
When you only need imperative animations without the declarative features of motion components, use useAnimate from framer-motion/mini. This provides the same animation capabilities at 2.3kb instead of requiring the full 17kb hybrid bundle.
Incorrect (full useAnimate for simple imperative animation):
// components/NotificationBadge.tsx
import { useAnimate } from "framer-motion"; // Pulls in 17kb hybrid bundle
export function NotificationBadge({ count }: NotificationBadgeProps) {
const [scope, animate] = useAnimate();
const handleNewNotification = async () => {
await animate(scope.current, { scale: 1.2 }, { duration: 0.1 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
};
return (
<div ref={scope} className="badge">
{count}
</div>
);
}Correct (mini useAnimate for simple imperative animation):
// components/NotificationBadge.tsx
import { useAnimate } from "framer-motion/mini"; // Only 2.3kb
export function NotificationBadge({ count }: NotificationBadgeProps) {
const [scope, animate] = useAnimate();
const handleNewNotification = async () => {
await animate(scope.current, { scale: 1.2 }, { duration: 0.1 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
};
return (
<div ref={scope} className="badge">
{count}
</div>
);
}When mini is sufficient:
- Imperative animations triggered by events
- Simple enter/exit animations on DOM elements
- Animations that do not need spring physics or complex sequencing
When to use full useAnimate:
- You need spring animations with custom damping/stiffness
- You are already using LazyMotion (no additional cost)
- You need to animate motion values or complex keyframes
Reference: Framer Motion - Mini Animate
Wrap Conditional Renders with AnimatePresence
React immediately removes components from the DOM when conditionally rendered out. AnimatePresence defers removal until exit animations complete, enabling smooth transitions when elements leave the screen.
Incorrect (exit animation never runs):
import { motion } from "framer-motion";
function NotificationBanner({ message, isVisible }: NotificationProps) {
return (
<>
{isVisible && (
<motion.div
className="notification"
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }} // Never runs - component unmounts immediately
>
{message}
</motion.div>
)}
</>
);
}Correct (AnimatePresence enables exit animation):
import { motion, AnimatePresence } from "framer-motion";
function NotificationBanner({ message, isVisible }: NotificationProps) {
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="notification"
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }} // Runs before unmount
>
{message}
</motion.div>
)}
</AnimatePresence>
);
}Reference: Framer Motion - AnimatePresence
Use mode="wait" for Sequential Page Transitions
By default, AnimatePresence renders entering and exiting elements simultaneously, causing layout overlap during transitions. Setting mode="wait" ensures the exiting element fully animates out before the entering element begins, preventing visual collisions.
Incorrect (overlapping transitions):
import { motion, AnimatePresence } from "framer-motion";
import { useLocation } from "react-router-dom";
function PageTransition({ children }: { children: React.ReactNode }) {
const location = useLocation();
return (
<AnimatePresence>
<motion.div
key={location.pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }} // Old and new pages render simultaneously, causing overlap
>
{children}
</motion.div>
</AnimatePresence>
);
}Correct (sequential with mode="wait"):
import { motion, AnimatePresence } from "framer-motion";
import { useLocation } from "react-router-dom";
function PageTransition({ children }: { children: React.ReactNode }) {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }} // Old page exits fully before new page enters
>
{children}
</motion.div>
</AnimatePresence>
);
}Mode options:
"sync"(default): Enter and exit animations happen simultaneously"wait": Exit completes before enter begins"popLayout": Exiting elements are popped from layout flow
Reference: Framer Motion - AnimatePresence
Provide Unique Keys for AnimatePresence Children
AnimatePresence uses React keys to track which elements enter and exit. Missing or duplicate keys cause incorrect animations, with elements animating in place instead of properly entering/exiting, or animations being skipped entirely.
Incorrect (missing or index-based keys):
import { motion, AnimatePresence } from "framer-motion";
function TodoList({ todos }: TodoListProps) {
return (
<ul>
<AnimatePresence>
{todos.map((todo, index) => (
<motion.li
key={index} // Index keys cause wrong item to animate on removal
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
>
{todo.text}
</motion.li>
))}
</AnimatePresence>
</ul>
);
}Correct (unique stable keys):
import { motion, AnimatePresence } from "framer-motion";
function TodoList({ todos }: TodoListProps) {
return (
<ul>
<AnimatePresence>
{todos.map((todo) => (
<motion.li
key={todo.id} // Unique ID tracks correct element for exit animation
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
>
{todo.text}
</motion.li>
))}
</AnimatePresence>
</ul>
);
}Reference: Framer Motion - AnimatePresence
Use dragConstraints Ref for Automatic Boundaries
The dragConstraints prop accepts a ref to a parent element, automatically calculating boundaries based on the container's dimensions. Manual pixel constraints require hardcoded values that break on different screen sizes and don't update when the container resizes.
Incorrect (manual boundary calculation):
function DraggableCard({ children }: DraggableCardProps) {
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const cardSize = { width: 200, height: 150 };
useEffect(() => {
// Must manually track container size
const updateSize = () => {
const container = document.getElementById("drag-container");
if (container) {
setContainerSize({
width: container.offsetWidth,
height: container.offsetHeight,
});
}
};
updateSize();
window.addEventListener("resize", updateSize); // Manual resize handling
return () => window.removeEventListener("resize", updateSize);
}, []);
return (
<div id="drag-container" className="container">
<motion.div
drag
dragConstraints={{ // Hardcoded calculations
top: 0,
left: 0,
right: containerSize.width - cardSize.width,
bottom: containerSize.height - cardSize.height,
}}
className="card"
>
{children}
</motion.div>
</div>
);
}Correct (ref-based automatic boundaries):
function DraggableCard({ children }: DraggableCardProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="container">
<motion.div
drag
dragConstraints={containerRef} // Automatic boundary calculation
className="card"
>
{children}
</motion.div>
</div>
);
}Reference: Framer Motion - Drag
Configure dragElastic for Natural Drag Feel
The dragElastic prop controls how much the element can be dragged past its constraints before snapping back. A value of 0 creates a hard stop that feels unresponsive, while values between 0.1-0.5 provide natural momentum that matches native app behavior.
Incorrect (hard stop at boundaries):
function SwipeableItem({ item, onDismiss }: SwipeableItemProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="swipe-container">
<motion.div
drag="x"
dragConstraints={containerRef}
dragElastic={0} // Hard stop - feels rigid and unnatural
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss(item.id);
}
}}
className="swipeable-item"
>
<span>{item.title}</span>
</motion.div>
</div>
);
}Correct (elastic overshoot for natural feel):
function SwipeableItem({ item, onDismiss }: SwipeableItemProps) {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="swipe-container">
<motion.div
drag="x"
dragConstraints={containerRef}
dragElastic={0.2} // Allows 20% overshoot - natural momentum feel
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onDismiss(item.id);
}
}}
className="swipeable-item"
>
<span>{item.title}</span>
</motion.div>
</div>
);
}Recommended values:
0- Hard stop (avoid unless intentional)0.1-0.2- Subtle overshoot, professional feel0.3-0.5- Noticeable elasticity, playful feel1- Full elasticity, equal to drag distance
Reference: Framer Motion - Drag
Use onTapCancel for Interrupted Gesture Cleanup
When a tap gesture is interrupted (pointer leaves element or becomes a drag), onTap never fires. Without onTapCancel, any state changes made in onTapStart won't be reverted, leaving the UI in an inconsistent state. Always pair onTapStart with both onTap and onTapCancel.
Incorrect (missing cancel handling):
function InteractiveCard({ item, onSelect }: InteractiveCardProps) {
const [isPressing, setIsPressing] = useState(false);
return (
<motion.div
className="interactive-card"
onTapStart={() => {
setIsPressing(true); // State set on tap start
playHapticFeedback();
}}
onTap={() => {
setIsPressing(false);
onSelect(item.id);
}}
// Missing onTapCancel - if user drags away, isPressing stays true!
animate={{ scale: isPressing ? 0.97 : 1 }}
>
<span>{item.title}</span>
</motion.div>
);
}Correct (complete gesture handling with cancel):
function InteractiveCard({ item, onSelect }: InteractiveCardProps) {
const [isPressing, setIsPressing] = useState(false);
return (
<motion.div
className="interactive-card"
onTapStart={() => {
setIsPressing(true);
playHapticFeedback();
}}
onTap={() => {
setIsPressing(false);
onSelect(item.id);
}}
onTapCancel={() => {
setIsPressing(false); // Cleanup when tap is interrupted
}}
animate={{ scale: isPressing ? 0.97 : 1 }}
>
<span>{item.title}</span>
</motion.div>
);
}When onTapCancel fires:
- Pointer leaves the element bounds
- Gesture transitions to a drag
- Another touch point is detected (multi-touch)
- The element is unmounted during the tap
Reference: Framer Motion - Gestures
Let Gesture Variants Flow to Children
Variants automatically propagate to children, so gesture states like whileHover flow down the component tree. Duplicating gesture props on parent and children is redundant and creates maintenance burden. Define variants once on the parent and reference them in children.
Incorrect (duplicated gesture props on parent and children):
function FeatureCard({ title, description, icon }: FeatureCardProps) {
return (
<motion.div
className="card"
whileHover={{ scale: 1.02 }} // Gesture defined here
>
<motion.div
className="icon-wrapper"
whileHover={{ rotate: 10 }} // Must manually add whileHover again
>
{icon}
</motion.div>
<motion.h3
whileHover={{ color: "#3b82f6" }} // Must manually add whileHover again
>
{title}
</motion.h3>
<p>{description}</p>
</motion.div>
);
}Correct (variants propagate to children automatically):
const cardVariants = {
rest: { scale: 1 },
hover: { scale: 1.02 },
};
const iconVariants = {
rest: { rotate: 0 },
hover: { rotate: 10 }, // Triggers when parent enters "hover"
};
const titleVariants = {
rest: { color: "#1f2937" },
hover: { color: "#3b82f6" }, // Triggers when parent enters "hover"
};
function FeatureCard({ title, description, icon }: FeatureCardProps) {
return (
<motion.div
className="card"
variants={cardVariants}
initial="rest"
whileHover="hover" // Children inherit this state automatically
>
<motion.div className="icon-wrapper" variants={iconVariants}>
{icon}
</motion.div>
<motion.h3 variants={titleVariants}>
{title}
</motion.h3>
<p>{description}</p>
</motion.div>
);
}Reference: Framer Motion - Variants
Use whileHover/whileTap Instead of Event Handlers
Framer Motion's whileHover and whileTap props are optimized internally to update styles without triggering React re-renders. Using onMouseEnter/onMouseLeave with useState causes unnecessary component re-renders and breaks memoization.
Incorrect (state updates cause re-renders):
function ActionButton({ label, onClick }: ActionButtonProps) {
const [isHovered, setIsHovered] = useState(false); // State triggers re-renders
const [isPressed, setIsPressed] = useState(false);
return (
<motion.button
onMouseEnter={() => setIsHovered(true)} // Re-render on hover
onMouseLeave={() => setIsHovered(false)} // Re-render on leave
onMouseDown={() => setIsPressed(true)} // Re-render on press
onMouseUp={() => setIsPressed(false)} // Re-render on release
animate={{
scale: isPressed ? 0.95 : isHovered ? 1.05 : 1,
backgroundColor: isHovered ? "#3b82f6" : "#2563eb",
}}
onClick={onClick}
>
{label}
</motion.button>
);
}Correct (built-in gesture props, no re-renders):
function ActionButton({ label, onClick }: ActionButtonProps) {
return (
<motion.button
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6" }} // Optimized internally
whileTap={{ scale: 0.95 }} // No state, no re-renders
initial={{ backgroundColor: "#2563eb" }}
onClick={onClick}
>
{label}
</motion.button>
);
}Reference: Framer Motion - Gestures
Use layoutDependency to Limit Layout Measurements
By default, layout animations measure element position on every render. The layoutDependency prop tells Framer Motion to only recalculate layout when specific values change, dramatically reducing unnecessary DOM measurements.
Incorrect (measures layout on every render):
function ProductList({ products, sortOrder }: Props) {
return (
<div className="grid">
{products.map((product) => (
<motion.div
key={product.id}
layout // Measures on EVERY render
className="product-card"
>
<ProductCard product={product} />
</motion.div>
))}
</div>
);
}Correct (measures only when sortOrder changes):
function ProductList({ products, sortOrder }: Props) {
return (
<div className="grid">
{products.map((product) => (
<motion.div
key={product.id}
layout
layoutDependency={sortOrder} // Only measures when sortOrder changes
className="product-card"
>
<ProductCard product={product} />
</motion.div>
))}
</div>
);
}When to use layoutDependency:
- Lists that reorder based on a specific trigger (sort, filter)
- Components where layout only changes with certain state values
- Performance-critical sections with many layout-animated elements
Note: Pass an array for multiple dependencies: layoutDependency={[sortOrder, filterType]}
Reference: Framer Motion - Layout Animations
Group Related Layout Animations with LayoutGroup
When multiple components have layout animations that should coordinate, wrap them in LayoutGroup. This batches layout measurements and ensures sibling components animate together without causing layout shift cascades.
Incorrect (separate layout animations cause cascading shifts):
function Dashboard() {
const [expanded, setExpanded] = useState<string | null>(null);
return (
<div className="dashboard">
{/* These animate independently, causing visual jank */}
<Sidebar expanded={expanded === "sidebar"} />
<motion.main layout className="content">
<WidgetGrid />
</motion.main>
<NotificationPanel expanded={expanded === "notifications"} />
</div>
);
}
function Sidebar({ expanded }: { expanded: boolean }) {
return (
<motion.aside layout className="sidebar">
{expanded && <SidebarDetails />}
</motion.aside>
);
}Correct (grouped animations coordinate measurements):
import { LayoutGroup } from "framer-motion";
function Dashboard() {
const [expanded, setExpanded] = useState<string | null>(null);
return (
<LayoutGroup>
<div className="dashboard">
{/* All layout animations now coordinate together */}
<Sidebar expanded={expanded === "sidebar"} />
<motion.main layout className="content">
<WidgetGrid />
</motion.main>
<NotificationPanel expanded={expanded === "notifications"} />
</div>
</LayoutGroup>
);
}
function Sidebar({ expanded }: { expanded: boolean }) {
return (
<motion.aside layout className="sidebar">
{expanded && <SidebarDetails />}
</motion.aside>
);
}Use cases for LayoutGroup:
- Dashboard layouts where expanding one panel affects others
- List items that reorder across multiple parent components
- Multi-column layouts with coordinated animations
- Shared element transitions between sibling components
Note: LayoutGroup also enables layoutId matching across component boundaries.
Reference: Framer Motion - LayoutGroup
Use layoutId for Shared Element Transitions
When an element should visually transition from one component to another (like a list item expanding to a modal), use matching layoutId props. Framer Motion automatically animates between the two positions using the FLIP technique.
Incorrect (manual animation with opacity/scale):
function ProductGrid({ products, onSelect }: Props) {
const [selected, setSelected] = useState<Product | null>(null);
return (
<>
<div className="grid">
{products.map((product) => (
<motion.div
key={product.id}
className="product-card"
onClick={() => setSelected(product)}
animate={{ opacity: selected ? 0.5 : 1 }} // Fade effect, not smooth
>
<img src={product.image} alt={product.name} />
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<motion.div
className="product-modal"
initial={{ opacity: 0, scale: 0.8 }} // Abrupt appearance
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
>
<img src={selected.image} alt={selected.name} />
</motion.div>
)}
</AnimatePresence>
</>
);
}Correct (layoutId for automatic shared element transition):
function ProductGrid({ products, onSelect }: Props) {
const [selected, setSelected] = useState<Product | null>(null);
return (
<>
<div className="grid">
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`} // Matching layoutId
className="product-card"
onClick={() => setSelected(product)}
>
<motion.img
layoutId={`image-${product.id}`} // Image also transitions
src={product.image}
alt={product.name}
/>
</motion.div>
))}
</div>
<AnimatePresence>
{selected && (
<motion.div
layoutId={`product-${selected.id}`} // Same layoutId - animates between!
className="product-modal"
>
<motion.img
layoutId={`image-${selected.id}`}
src={selected.image}
alt={selected.name}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
}Key points:
- Only one element with a given
layoutIdshould be mounted at a time - Wrap exiting elements in
AnimatePresencefor smooth exit transitions - Nest multiple
layoutIdelements for complex shared transitions (card + image + title) - Use
LayoutGroupwhenlayoutIdelements are in different component trees
Reference: Framer Motion - Shared Layout Animations
Use layout="position" or "size" for Targeted Animations
When only position OR size changes (not both), use layout="position" or layout="size" instead of layout={true}. This tells Framer Motion to skip measuring and animating the unchanged dimension, reducing computation.
Incorrect (animates both position and size unnecessarily):
function TabPanel({ tabs, activeIndex }: Props) {
return (
<div className="tab-container">
{tabs.map((tab, index) => (
<motion.div
key={tab.id}
layout={true} // Animates position AND size
className="tab"
>
{tab.label}
</motion.div>
))}
<motion.div
layout={true} // Only position changes, size stays constant
className="tab-indicator"
style={{ width: TAB_WIDTH }}
/>
</div>
);
}Correct (animates only position for the indicator):
function TabPanel({ tabs, activeIndex }: Props) {
return (
<div className="tab-container">
{tabs.map((tab, index) => (
<motion.div
key={tab.id}
layout={true}
className="tab"
>
{tab.label}
</motion.div>
))}
<motion.div
layout="position" // Only animates position, skips size calculations
className="tab-indicator"
style={{ width: TAB_WIDTH }}
/>
</div>
);
}Guidelines:
layout="position"- Use when element moves but size stays constant (tab indicators, drag-and-drop)layout="size"- Use when element resizes in place but doesn't move (accordion panels, expandable cards)layout={true}- Use when both position and size change (grid reflows, responsive layouts)
Reference: Framer Motion - Layout Prop
Add layoutScroll to Scrollable Ancestors
Layout animations calculate element positions relative to the viewport. When a layout-animated element is inside a scrollable container, the scroll offset can cause incorrect position calculations. Add layoutScroll to the scrollable ancestor to account for scroll position.
Incorrect (layout animation jumps due to scroll offset):
function MessageList({ messages }: Props) {
return (
<div className="message-container" style={{ overflowY: "auto", height: 400 }}>
{/* When scrolled, new messages animate from wrong position */}
{messages.map((message) => (
<motion.div
key={message.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="message"
>
{message.text}
</motion.div>
))}
</div>
);
}Correct (layoutScroll on scrollable container):
function MessageList({ messages }: Props) {
return (
<motion.div
layoutScroll // Accounts for scroll offset in layout calculations
className="message-container"
style={{ overflowY: "auto", height: 400 }}
>
{messages.map((message) => (
<motion.div
key={message.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="message"
>
{message.text}
</motion.div>
))}
</motion.div>
);
}When to use layoutScroll:
- Scrollable lists with reorderable items
- Chat interfaces with layout-animated messages
- Infinite scroll feeds with layout transitions
- Any
overflow: auto/scrollcontainer with layout animations inside
Note: Apply layoutScroll to ALL scrollable ancestors in the hierarchy, not just the immediate parent. For deeply nested scroll containers, each scrollable element needs the prop.
<motion.div layoutScroll style={{ overflowY: "auto" }}>
<motion.div layoutScroll style={{ overflowX: "auto" }}>
<motion.div layout>Content</motion.div>
</motion.div>
</motion.div>Reference: Framer Motion - Layout Scroll
Use Stable Animate Values to Prevent Animation Restarts
Passing dynamic objects directly to the animate prop creates new references on every render, causing Framer Motion to restart the animation. Use variant names (strings) or memoized objects to ensure animations only restart when intentionally triggered.
Incorrect (dynamic object restarts animation on every render):
function PulsingButton({ isActive }) {
const [clicks, setClicks] = useState(0);
return (
<motion.button
animate={{ // New object on every render, animation restarts
scale: isActive ? 1.1 : 1,
backgroundColor: isActive ? '#10b981' : '#6b7280',
}}
transition={{ duration: 0.3 }}
onClick={() => setClicks(c => c + 1)}
>
Clicked {clicks} times
</motion.button>
);
}Correct (variant name provides stable reference):
const buttonVariants = {
active: { scale: 1.1, backgroundColor: '#10b981' },
inactive: { scale: 1, backgroundColor: '#6b7280' },
};
function PulsingButton({ isActive }) {
const [clicks, setClicks] = useState(0);
return (
<motion.button
variants={buttonVariants}
animate={isActive ? 'active' : 'inactive'} // String reference is stable
transition={{ duration: 0.3 }}
onClick={() => setClicks(c => c + 1)}
>
Clicked {clicks} times
</motion.button>
);
}Reference: Framer Motion - Animation
Use Motion Value Events Instead of useEffect
Motion values provide event callbacks (onChange, onAnimationStart, onAnimationComplete) that execute outside React's render cycle. Using useEffect to watch motion value changes and sync to React state causes unnecessary re-renders. Use motion value events for side effects that do not need to trigger re-renders.
Incorrect (useEffect syncs to state, causing re-renders):
function DragIndicator() {
const x = useMotionValue(0);
const [isDraggingRight, setIsDraggingRight] = useState(false);
useEffect(() => {
const unsubscribe = x.on('change', (latest) => {
setIsDraggingRight(latest > 0); // Re-renders on every frame
});
return unsubscribe;
}, [x]);
return (
<motion.div drag="x" style={{ x }}>
{isDraggingRight ? 'Moving right' : 'Moving left'}
</motion.div>
);
}Correct (motion value event updates DOM directly):
function DragIndicator() {
const x = useMotionValue(0);
const indicatorRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const unsubscribe = x.on('change', (latest) => {
if (indicatorRef.current) {
indicatorRef.current.textContent = latest > 0 ? 'Moving right' : 'Moving left'; // Direct DOM update
}
});
return unsubscribe;
}, [x]);
return (
<motion.div drag="x" style={{ x }}>
<span ref={indicatorRef}>Moving left</span>
</motion.div>
);
}Reference: Framer Motion - Motion Value Events
Use useMotionValue Instead of useState for Animated Values
Motion values update the DOM directly without triggering React re-renders. Using useState for animated values causes a full component re-render on every animation frame (60+ times per second), leading to severe performance degradation and janky animations.
Incorrect (useState causes re-renders every frame):
function DraggableCard() {
const [x, setX] = useState(0); // Re-renders on every drag frame
return (
<motion.div
drag="x"
style={{ x }}
onDrag={(_, info) => setX(info.point.x)} // 60+ setState calls per second
>
<ExpensiveChildComponent />
</motion.div>
);
}Correct (useMotionValue bypasses React):
function DraggableCard() {
const x = useMotionValue(0); // Updates DOM directly, no re-renders
return (
<motion.div
drag="x"
style={{ x }}
// No onDrag needed - motion value updates automatically
>
<ExpensiveChildComponent />
</motion.div>
);
}Reference: Framer Motion - Motion Values
Keep Animation Callbacks Stable with useCallback
Inline callback functions create new function references on every render, breaking memoization of child components and causing unnecessary re-renders. Animation callbacks like onAnimationComplete, onDragEnd, and onHoverStart should be memoized with useCallback to maintain referential equality.
Incorrect (inline callback breaks memoization):
function AnimatedList({ items }) {
const [selected, setSelected] = useState(null);
return (
<motion.ul>
{items.map((item) => (
<MemoizedListItem
key={item.id}
item={item}
onAnimationComplete={() => console.log(`${item.id} animated`)} // New function every render
/>
))}
</motion.ul>
);
}Correct (memoized callback preserves child memoization):
function AnimatedList({ items }) {
const [selected, setSelected] = useState(null);
const handleAnimationComplete = useCallback((id: string) => {
console.log(`${id} animated`);
}, []);
return (
<motion.ul>
{items.map((item) => (
<MemoizedListItem
key={item.id}
item={item}
onAnimationComplete={() => handleAnimationComplete(item.id)} // Stable reference
/>
))}
</motion.ul>
);
}Reference: React - useCallback
Derive Values with useTransform Instead of useEffect
useTransform creates derived motion values that update synchronously with their source, completely bypassing React's render cycle. Using useEffect with setState to derive values causes unnecessary re-renders and creates visual lag between the source and derived values.
Incorrect (useEffect creates render cycle and lag):
function ScrollProgress() {
const { scrollYProgress } = useScroll();
const [opacity, setOpacity] = useState(1);
useEffect(() => {
const unsubscribe = scrollYProgress.on('change', (v) => {
setOpacity(1 - v); // Triggers re-render on every scroll frame
});
return unsubscribe;
}, [scrollYProgress]);
return <motion.div style={{ opacity }} />;
}Correct (useTransform derives value without re-renders):
function ScrollProgress() {
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 1], [1, 0]); // No re-renders
return <motion.div style={{ opacity }} />;
}Reference: Framer Motion - useTransform
Define Variants Outside Component or with useMemo
Defining variants objects inside a component creates new object references on every render. Framer Motion detects these as "new" variants and may restart animations or cause unnecessary diffing. Define variants outside the component or memoize them to maintain stable references.
Incorrect (inline variants create new reference each render):
function FadeInCard({ isVisible }) {
const [count, setCount] = useState(0);
const cardVariants = { // New object created on every render
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate={isVisible ? 'visible' : 'hidden'}
/>
);
}Correct (variants defined outside component):
const cardVariants = { // Stable reference, created once
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function FadeInCard({ isVisible }) {
const [count, setCount] = useState(0);
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate={isVisible ? 'visible' : 'hidden'}
/>
);
}Reference: Framer Motion - Variants
Track Scroll Within Specific Containers Using container Option
By default, useScroll tracks the page scroll. To track scroll progress within a specific scrollable container (like a sidebar, modal, or nested scroll area), pass a container ref. Without this, scroll animations in nested containers won't respond to the container's scroll position.
Incorrect (tracks page scroll instead of container):
import { useRef } from "react";
import { motion, useScroll } from "framer-motion";
function ScrollableGallery() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollXProgress } = useScroll(); // Tracks page scroll, not container
return (
<div
ref={containerRef}
className="gallery"
style={{ overflowX: "auto", display: "flex" }}
>
{images.map((src, i) => (
<img key={i} src={src} alt="" />
))}
<motion.div
className="scroll-indicator"
style={{ scaleX: scrollXProgress }} // Doesn't respond to gallery scroll
/>
</div>
);
}Correct (container ref tracks nested scroll):
import { useRef } from "react";
import { motion, useScroll } from "framer-motion";
function ScrollableGallery() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollXProgress } = useScroll({
container: containerRef // Tracks this container's horizontal scroll
});
return (
<div
ref={containerRef}
className="gallery"
style={{ overflowX: "auto", display: "flex" }}
>
{images.map((src, i) => (
<img key={i} src={src} alt="" />
))}
<motion.div
className="scroll-indicator"
style={{ scaleX: scrollXProgress }} // Responds to gallery scroll
/>
</div>
);
}Combining container and target:
const { scrollYProgress } = useScroll({
container: scrollContainerRef, // Track scroll of this container
target: elementRef, // Track this element's position within container
offset: ["start end", "end start"]
});Reference: Framer Motion - useScroll Container
Track Specific Elements Entering Viewport with useScroll target
The useScroll hook accepts a target ref to track a specific element's position relative to the viewport. This provides scroll progress from 0 to 1 as the element enters and exits the viewport. Manual IntersectionObserver setup is verbose and doesn't provide continuous progress values.
Incorrect (manual IntersectionObserver setup):
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
function RevealSection() {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ threshold: 0.5 }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []); // Verbose setup, binary visibility only
return (
<motion.div
ref={ref}
animate={{ opacity: isVisible ? 1 : 0 }} // Abrupt transition, no progress
>
<h2>Section Content</h2>
</motion.div>
);
}Correct (useScroll with target ref):
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
function RevealSection() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
}); // Continuous progress as element scrolls through viewport
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
return (
<motion.div ref={ref} style={{ opacity }}>
<h2>Section Content</h2>
</motion.div>
);
}Reference: Framer Motion - useScroll Element Position
Configure Scroll Offsets for Precise Animation Triggers
The offset option in useScroll defines when scroll progress starts (0) and ends (1) relative to the target element and viewport. Without custom offsets, animations may trigger too early or too late, causing awkward timing as users scroll.
Incorrect (default offset starts animation too early):
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
function FadeInCard() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref
// Default offset: ["start start", "end end"]
// Animation completes before card is fully visible
});
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1]);
return (
<motion.div ref={ref} className="card" style={{ opacity }}>
<p>Card content fades in awkwardly</p>
</motion.div>
);
}Correct (custom offset for natural reveal):
import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";
function FadeInCard() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "start center"]
// Starts when element top enters viewport bottom
// Ends when element top reaches viewport center
});
const opacity = useTransform(scrollYProgress, [0, 1], [0, 1]);
return (
<motion.div ref={ref} className="card" style={{ opacity }}>
<p>Card content fades in naturally</p>
</motion.div>
);
}Offset format: ["<target> <viewport>", "<target> <viewport>"]
- First value: when progress equals 0
- Second value: when progress equals 1
- Keywords:
start,center,end, or pixel/percentage values
Common offset patterns:
["start end", "end start"]- full viewport traversal["start end", "start center"]- reveal as element enters["center center", "end start"]- animate from center to exit
Reference: Framer Motion - useScroll Offsets
Use useScroll Hook for Scroll-Linked Animations
The useScroll hook returns motion values that update via the browser's native ScrollTimeline API, enabling hardware-accelerated animations that remain smooth even during heavy JavaScript execution. Manual scroll event listeners run on the main thread and can cause significant jank.
Incorrect (manual scroll event listener blocks main thread):
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
function ProgressBar() {
const [scrollProgress, setScrollProgress] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
setScrollProgress(scrollTop / docHeight); // Re-renders on every scroll event
};
window.addEventListener("scroll", handleScroll); // Blocks main thread
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<motion.div
className="progress-bar"
style={{ scaleX: scrollProgress, transformOrigin: "left" }}
/>
);
}Correct (useScroll with hardware acceleration):
import { motion, useScroll } from "framer-motion";
function ProgressBar() {
const { scrollYProgress } = useScroll(); // Hardware-accelerated, no re-renders
return (
<motion.div
className="progress-bar"
style={{ scaleX: scrollYProgress, transformOrigin: "left" }}
/>
);
}Reference: Framer Motion - useScroll
Smooth Scroll Animations with useSpring
Raw scroll progress values change abruptly with each scroll event, causing jittery animations especially on trackpads or during fast scrolling. Wrapping scroll progress in useSpring adds physics-based smoothing that creates fluid, natural-feeling animations.
Incorrect (raw scrollYProgress causes jittery animations):
import { motion, useScroll } from "framer-motion";
function ParallaxSection() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="parallax-bg"
style={{ y: scrollYProgress }} // Jittery, updates abruptly on each scroll tick
/>
);
}Correct (useSpring smooths the animation):
import { motion, useScroll, useSpring } from "framer-motion";
function ParallaxSection() {
const { scrollYProgress } = useScroll();
const smoothProgress = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
return (
<motion.div
className="parallax-bg"
style={{ y: smoothProgress }} // Smooth, physics-based interpolation
/>
);
}Reference: Framer Motion - useSpring
Configure Damping to Control Oscillation
Damping controls how quickly a spring settles. Under-damped springs (low damping) oscillate excessively, making UI elements bounce repeatedly before settling. This delays user interaction and feels unprofessional. Appropriate damping ensures elements settle quickly while maintaining a natural feel.
Incorrect (under-damped, excessive bouncing):
import { motion } from "framer-motion";
function TooltipPopover({ isVisible }: { isVisible: boolean }) {
return (
<motion.div
className="tooltip"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 10 }}
transition={{ type: "spring", stiffness: 400, damping: 8 }} // Bounces 3-4 times before settling
>
<p>Helpful tooltip text</p>
</motion.div>
);
}Correct (appropriately damped, settles quickly):
import { motion } from "framer-motion";
function TooltipPopover({ isVisible }: { isVisible: boolean }) {
return (
<motion.div
className="tooltip"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: isVisible ? 1 : 0, y: isVisible ? 0 : 10 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }} // Settles with minimal overshoot
>
<p>Helpful tooltip text</p>
</motion.div>
);
}Damping guidelines:
damping < 10: Very bouncy, use sparingly for playful UIdamping 15-25: Slight overshoot, natural feel for most UIdamping > 30: No overshoot, quick settle for functional elements- Critical damping:
damping = 2 * sqrt(stiffness * mass)for zero oscillation
Reference: Framer Motion - Spring
Adjust Mass for Heavier or Lighter Animation Feel
Mass controls the inertia of animated elements. Default mass (1) works for small UI elements, but large elements like fullscreen modals or heavy cards feel unnaturally light and jittery. Increasing mass makes animations feel weightier and more grounded, matching user expectations of physical objects.
Incorrect (default mass on large element):
import { motion } from "framer-motion";
function FullscreenDrawer({ isOpen }: { isOpen: boolean }) {
return (
<motion.div
className="drawer-fullscreen"
initial={{ x: "100%" }}
animate={{ x: isOpen ? 0 : "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }} // Feels too light for fullscreen element
>
<div className="drawer-content">
<h2>Settings Panel</h2>
<nav>Navigation items</nav>
</div>
</motion.div>
);
}Correct (increased mass for weight):
import { motion } from "framer-motion";
function FullscreenDrawer({ isOpen }: { isOpen: boolean }) {
return (
<motion.div
className="drawer-fullscreen"
initial={{ x: "100%" }}
animate={{ x: isOpen ? 0 : "100%" }}
transition={{ type: "spring", stiffness: 300, damping: 30, mass: 1.5 }} // Feels appropriately heavy
>
<div className="drawer-content">
<h2>Settings Panel</h2>
<nav>Navigation items</nav>
</div>
</motion.div>
);
}Mass guidelines:
mass: 0.5-0.8: Light, snappy micro-interactions (buttons, icons)mass: 1: Default, suitable for cards, menus, tooltipsmass: 1.2-2: Heavy elements (drawers, modals, large panels)- Higher mass requires proportionally higher stiffness to maintain responsiveness
Reference: Framer Motion - Spring
Use Physics-Based Springs for Interruptible Animations
Physics-based springs (stiffness/damping) simulate real-world motion and handle interruptions gracefully by preserving velocity. Duration-based transitions feel robotic and reset abruptly when interrupted mid-animation, breaking the illusion of physical objects.
Incorrect (duration-based, resets on interruption):
import { motion } from "framer-motion";
function ModalDialog({ isOpen }: { isOpen: boolean }) {
return (
<motion.div
className="modal"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: isOpen ? 1 : 0.8, opacity: isOpen ? 1 : 0 }}
transition={{ duration: 0.3, ease: "easeOut" }} // Resets abruptly if toggled mid-animation
>
<div className="modal-content">Dialog content</div>
</motion.div>
);
}Correct (physics-based, preserves velocity):
import { motion } from "framer-motion";
function ModalDialog({ isOpen }: { isOpen: boolean }) {
return (
<motion.div
className="modal"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: isOpen ? 1 : 0.8, opacity: isOpen ? 1 : 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }} // Smoothly reverses preserving momentum
>
<div className="modal-content">Dialog content</div>
</motion.div>
);
}Spring parameters:
stiffness: Higher = faster, snappier (default: 100)damping: Higher = less oscillation (default: 10)mass: Higher = heavier, slower (default: 1)
Reference: Framer Motion - Transitions
Use useSpring for Reactive Spring Values
The useSpring hook creates a motion value that animates with spring physics whenever its target changes. Without it, derived values or externally-controlled animations jump abruptly to new values. useSpring ensures smooth, physics-based transitions that feel natural and can be interrupted gracefully.
Incorrect (abrupt value changes):
import { motion, useMotionValue, useTransform } from "framer-motion";
function ProgressIndicator({ progress }: { progress: number }) {
const scaleX = useMotionValue(progress); // Jumps instantly when progress changes
// Update motion value imperatively
useEffect(() => {
scaleX.set(progress); // Abrupt jump, no animation
}, [progress, scaleX]);
return (
<div className="progress-container">
<motion.div className="progress-bar" style={{ scaleX, originX: 0 }} />
<span>{Math.round(progress * 100)}%</span>
</div>
);
}Correct (smooth spring transitions):
import { motion, useSpring, useTransform } from "framer-motion";
function ProgressIndicator({ progress }: { progress: number }) {
const scaleX = useSpring(progress, { stiffness: 100, damping: 20 }); // Animates smoothly to new value
// useSpring automatically animates when progress prop changes
return (
<div className="progress-container">
<motion.div className="progress-bar" style={{ scaleX, originX: 0 }} />
<span>{Math.round(progress * 100)}%</span>
</div>
);
}useSpring options:
- Accepts same spring config:
stiffness,damping,mass - Can accept a MotionValue as source:
useSpring(motionValue, config) - Returns a MotionValue that can be used in style props or transformed
Reference: Framer Motion - useSpring
Match Point Counts for Smooth Path Morphing
When morphing between SVG paths, matching the number and type of path commands ensures smooth interpolation. Mismatched paths require Framer Motion to guess intermediate points, causing unpredictable visual artifacts and more expensive calculations.
Incorrect (mismatched path points):
import { motion } from "framer-motion";
function MorphingIcon({ isActive }: { isActive: boolean }) {
// Circle: 4 points, Star: 10 points - causes interpolation issues
const circlePath = "M50,25 A25,25 0 1,1 50,75 A25,25 0 1,1 50,25";
const starPath = "M50,5 L61,40 L98,40 L68,62 L79,97 L50,75 L21,97 L32,62 L2,40 L39,40 Z";
return (
<svg viewBox="0 0 100 100" width={100} height={100}>
<motion.path
d={isActive ? starPath : circlePath} // Point count mismatch causes glitchy morph
fill="gold"
transition={{ duration: 0.5 }}
/>
</svg>
);
}Correct (matched path points):
import { motion } from "framer-motion";
function MorphingIcon({ isActive }: { isActive: boolean }) {
// Both paths have 10 points for smooth interpolation
const circlePath = "M50,25 L50,25 L75,50 L75,50 L50,75 L50,75 L25,50 L25,50 L50,25 L50,25";
const starPath = "M50,5 L61,40 L98,40 L68,62 L79,97 L50,75 L21,97 L32,62 L2,40 L39,40";
return (
<svg viewBox="0 0 100 100" width={100} height={100}>
<motion.path
d={isActive ? starPath : circlePath} // Matched points morph smoothly
fill="gold"
transition={{ duration: 0.5 }}
/>
</svg>
);
}Tip: Use tools like flubber or design both shapes with the same point count in your SVG editor.
Reference: Framer Motion - Animating SVG paths
Use motion.path and motion.circle for SVG Animation
Framer Motion's SVG components (motion.path, motion.circle, motion.rect, etc.) handle SVG-specific quirks like transform origin and attribute animation. Regular SVG elements with CSS animations have inconsistent transform origin behavior across browsers.
Incorrect (CSS animation on SVG elements):
import { useState } from "react";
function PulsingCircle() {
const [isActive, setIsActive] = useState(false);
return (
<svg viewBox="0 0 100 100" width={100} height={100}>
<circle
cx={50}
cy={50}
r={20}
fill="blue"
style={{
transform: isActive ? "scale(1.5)" : "scale(1)", // Transform origin issues in SVG
transition: "transform 0.3s",
}}
onClick={() => setIsActive(!isActive)}
/>
</svg>
);
}Correct (motion.circle with proper transforms):
import { motion } from "framer-motion";
import { useState } from "react";
function PulsingCircle() {
const [isActive, setIsActive] = useState(false);
return (
<svg viewBox="0 0 100 100" width={100} height={100}>
<motion.circle
cx={50}
cy={50}
r={20}
fill="blue"
animate={{ scale: isActive ? 1.5 : 1 }} // Handles transform origin correctly
transition={{ duration: 0.3 }}
onClick={() => setIsActive(!isActive)}
/>
</svg>
);
}Reference: Framer Motion - SVG
Use pathLength for Line Drawing Animations
The pathLength property normalizes any path to a length of 1, enabling simple 0-1 progress animations. Without it, you must calculate the exact path length and manage complex strokeDasharray values that vary per path.
Incorrect (manual strokeDasharray calculation):
import { motion } from "framer-motion";
function CheckmarkIcon() {
// Must calculate exact path length (varies per path)
const pathLength = 24.5; // Manual measurement required
return (
<svg viewBox="0 0 24 24" width={24} height={24}>
<motion.path
d="M4 12l6 6L20 6"
fill="none"
stroke="currentColor"
strokeWidth={2}
initial={{ strokeDasharray: pathLength, strokeDashoffset: pathLength }}
animate={{ strokeDashoffset: 0 }} // Breaks if path changes
transition={{ duration: 0.5 }}
/>
</svg>
);
}Correct (normalized pathLength):
import { motion } from "framer-motion";
function CheckmarkIcon() {
return (
<svg viewBox="0 0 24 24" width={24} height={24}>
<motion.path
d="M4 12l6 6L20 6"
fill="none"
stroke="currentColor"
strokeWidth={2}
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }} // Works for any path, no calculation needed
transition={{ duration: 0.5 }}
/>
</svg>
);
}Reference: Framer Motion - Path Animations
Animate viewBox for Smooth Zoom Effects
Animating the SVG viewBox attribute creates true zoom effects that maintain crisp vector quality at any zoom level. Using CSS scale transforms on SVG containers causes blurry edges and pixelation because it scales the rasterized output rather than the vector data.
Incorrect (scale transform causes blur):
import { motion } from "framer-motion";
function ZoomableChart({ data }: ChartProps) {
const [isZoomed, setIsZoomed] = useState(false);
return (
<motion.svg
viewBox="0 0 400 300"
width={400}
height={300}
animate={{ scale: isZoomed ? 2 : 1 }} // Scales rasterized output, causes blur
style={{ transformOrigin: "center" }}
onClick={() => setIsZoomed(!isZoomed)}
>
<ChartContent data={data} />
</motion.svg>
);
}Correct (viewBox animation maintains vector quality):
import { motion } from "framer-motion";
function ZoomableChart({ data }: ChartProps) {
const [isZoomed, setIsZoomed] = useState(false);
return (
<motion.svg
width={400}
height={300}
animate={{
viewBox: isZoomed ? "100 75 200 150" : "0 0 400 300", // True vector zoom, always crisp
}}
transition={{ duration: 0.5 }}
onClick={() => setIsZoomed(!isZoomed)}
>
<ChartContent data={data} />
</motion.svg>
);
}Reference: MDN - SVG viewBox
Related skills
How it compares
Use framer-motion for Framer Motion-specific React performance; reach for general React performance skills when animations are CSS-only or use a different library.
FAQ
How many rules does the framer-motion skill include?
The framer-motion skill includes 42 performance optimization rules organized into 9 categories, prioritized by impact to guide refactoring and automated code generation in React apps.
When should framer-motion be applied?
framer-motion should be applied when writing, reviewing, or refactoring React animations involving motion components, gestures, layout transitions, scroll-linked effects, or SVG animations with Framer Motion.
Is Framer Motion safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.