
Motion
- 793 installs
- 196 repo stars
- Updated July 25, 2026
- secondsky/claude-skills
motion is an agent skill that guides accessible Framer Motion implementations respecting prefers-reduced-motion, keyboard navigation, and screen readers for developers shipping animated React UIs.
About
motion is a skill from secondsky/claude-skills that documents how to make Motion (Framer Motion) animations accessible. It covers respecting prefers-reduced-motion for users with vestibular disorders, keyboard navigation support, ARIA integration, and accessibility testing workflows. The guide explains OS-level Reduce Motion settings and why animated UIs must degrade gracefully. Developers reach for motion when shipping React interfaces with Motion animations that must pass accessibility review for motion sensitivity, keyboard-only use, and screen reader compatibility.
- Respects prefers-reduced-motion for vestibular, attention, and epilepsy needs
- MotionConfig reducedMotion="user" wrapper for entire React apps
- Keyboard navigation support patterns
- ARIA integration guidelines for animated components
- Platform-specific OS instructions for macOS, Windows, iOS, and Android
Motion by the numbers
- 793 all-time installs (skills.sh)
- +25 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #478 of 1,888 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill motionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 793 |
|---|---|
| repo stars | ★ 196 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 25, 2026 |
| Repository | secondsky/claude-skills ↗ |
How do you make Framer Motion animations accessible?
Ensure all Framer Motion animations respect user accessibility settings and provide keyboard and screen reader support.
Who is it for?
Frontend developers shipping Framer Motion animations who must meet accessibility requirements for motion, keyboard, and screen readers.
Skip if: Static pages with no animation or backends with no Motion-based UI components to audit.
When should I use this skill?
The user builds or reviews Motion animations and needs prefers-reduced-motion, keyboard, ARIA, or accessibility testing guidance.
What you get
Reduced-motion-safe animations, keyboard-navigable motion UI, ARIA attributes, and accessibility test coverage.
- accessible motion components
- reduced-motion fallbacks
- a11y test checklist
Files
Motion Animation Library
Overview
Motion (package: motion, formerly framer-motion) is the industry-standard React animation library used in production by thousands of applications. With 30,200+ GitHub stars and 300+ official examples, it provides a declarative API for creating sophisticated animations with minimal code.
Key Capabilities:
- Gestures: drag, hover, tap, pan, focus with cross-device support
- Scroll Animations: viewport-triggered, scroll-linked, parallax effects
- Layout Animations: FLIP technique for smooth layout changes, shared element transitions
- Spring Physics: Natural, customizable motion with physics-based easing
- SVG: Path morphing, line drawing, attribute animation
- Exit Animations: AnimatePresence for unmounting transitions
- Performance: Hardware-accelerated, ScrollTimeline API, bundle optimization (2.3 KB - 34 KB)
Production Tested: React 19, Next.js 15, Vite 6, Tailwind v4
---
When to Use This Skill
✅ Use Motion When:
Complex Interactions:
- Drag-and-drop interfaces (sortable lists, kanban boards, sliders)
- Hover states with scale/rotation/color changes
- Tap feedback with bounce/squeeze effects
- Pan gestures for mobile-friendly controls
Scroll-Based Animations:
- Hero sections with parallax layers
- Scroll-triggered reveals (fade in as elements enter viewport)
- Progress bars linked to scroll position
- Sticky headers with scroll-dependent transforms
Layout Transitions:
- Shared element transitions between routes (card → detail page)
- Expand/collapse with automatic height animation
- Grid/list view switching with smooth repositioning
- Tab navigation with animated underline
Advanced Features:
- SVG line drawing animations
- Path morphing between shapes
- Spring physics for natural bounce
- Orchestrated sequences (staggered reveals)
- Modal dialogs with backdrop blur
Bundle Optimization:
- Need 2.3 KB animation library (useAnimate mini)
- Want to reduce Motion from 34 KB to 4.6 KB (LazyMotion)
❌ Don't Use Motion When:
- Simple list animations (use
auto-animateinstead: 3.28 KB vs 34 KB) - Static content without interactions
- Cloudflare Workers (use
framer-motionv12.23.24 workaround - see Known Issues) - 3D animations (use Three.js or React Three Fiber instead)
---
Installation
Latest Stable Version
bun add motion # preferred
# or: npm install motion
# or: yarn add motionCurrent Version: 12.23.24 (verified 2025-11-07)
Alternative for Cloudflare Workers:
# Use framer-motion if deploying to Cloudflare Workers
bun add framer-motion
# or: npm install framer-motionPackage Information
- Bundle Size:
- Full
motioncomponent: ~34 KB minified+gzipped LazyMotion+mcomponent: ~4.6 KBuseAnimatemini: 2.3 KB (smallest React animation library)useAnimatehybrid: 17 KB- Dependencies: React 18+ or React 19+
- TypeScript: Native support included (no @types package needed)
---
Core Concepts
1. The motion Component
Transform any HTML/SVG element into an animatable component:
import { motion } from "motion/react"
// Basic animation
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Content fades in and slides up
</motion.div>
// Gesture controls
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
Click me
</motion.button>Props:
initial: Starting state (object or variant name)animate: Target state (object or variant name)exit: Unmounting state (requires AnimatePresence)transition: Timing/easing configurationwhileHover,whileTap,whileFocus: Gesture stateswhileInView: Viewport-triggered animationdrag: Enable dragging ("x", "y", or true for both)layout: Enable FLIP layout animations
2. Variants (Animation Orchestration)
Named animation states that propagate through component tree:
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
<motion.div variants={variants} initial="hidden" animate="visible">
Content
</motion.div>For advanced orchestration (staggerChildren, delayChildren, dynamic variants), load references/core-concepts-deep-dive.md.
3. AnimatePresence (Exit Animations)
Enables animations when components unmount:
import { AnimatePresence } from "motion/react"
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>Critical Rules:
- AnimatePresence must stay mounted (don't wrap in conditional)
- All children must have unique `key` props
- AnimatePresence wraps the conditional, not the other way around
Common Mistake (exit animation won't play):
// ❌ Wrong - AnimatePresence unmounts with condition
{isVisible && (
<AnimatePresence>
<motion.div>Content</motion.div>
</AnimatePresence>
)}
// ✅ Correct - AnimatePresence stays mounted
<AnimatePresence>
{isVisible && <motion.div key="unique">Content</motion.div>}
</AnimatePresence>4. Layout Animations (FLIP)
Automatically animate layout changes:
<motion.div layout>
{isExpanded ? <FullContent /> : <Summary />}
</motion.div>Special props: layoutId (shared element transitions), layoutScroll (scrollable containers), layoutRoot (fixed positioning).
For advanced patterns (LayoutGroup, layoutId orchestration), load references/core-concepts-deep-dive.md.
5. Scroll Animations
// Viewport-triggered
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
Fades in when entering viewport
</motion.div>
// Scroll-linked (parallax)
import { useScroll, useTransform } from "motion/react"
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -300])
<motion.div style={{ y }}>Parallax effect</motion.div>For advanced scroll patterns (useScroll offsets, useTransform easing, parallax layers), load references/core-concepts-deep-dive.md.
6. Gestures
<motion.div drag="x" dragConstraints={{ left: -200, right: 200 }}>
Drag me
</motion.div>Available: whileHover, whileTap, whileFocus, whileDrag, whileInView, drag.
For advanced drag controls (momentum, elastic, event handlers), load references/core-concepts-deep-dive.md.
7. Spring Physics
<motion.div
animate={{ x: 100 }}
transition={{ type: "spring", stiffness: 100, damping: 10 }}
/>Common presets: Bouncy { stiffness: 300, damping: 10 }, Smooth { stiffness: 100, damping: 20 }.
For spring tuning (mass, visualizer, presets), load references/core-concepts-deep-dive.md.
---
Integration Guides
Vite: bun add motion → import { motion } from "motion/react" (works out of the box)
Next.js App Router: Requires "use client" directive or client component wrapper
"use client"
import { motion } from "motion/react"Tailwind: ⚠️ Remove transition-* classes (causes conflicts with Motion animations)
Cloudflare Workers: Use framer-motion v12.23.24 instead (Motion has Wrangler build issues)
For complete integration guides (Next.js patterns, SSR, framework-specific issues), load references/nextjs-integration.md.
---
Performance Optimization
Bundle Size: Use LazyMotion (34 KB → 4.6 KB):
import { LazyMotion, domAnimation, m } from "motion/react"
<LazyMotion features={domAnimation}>
<m.div>Only 4.6 KB!</m.div>
</LazyMotion>Large Lists: Use virtualization (react-window, react-virtuoso) for 50+ animated items.
For complete optimization guide (hardware acceleration, memory profiling, production benchmarks), load references/performance-optimization.md.
---
Accessibility
Respect `prefers-reduced-motion`:
import { MotionConfig } from "motion/react"
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>Keyboard Support: Use whileFocus for keyboard-triggered animations.
<motion.button whileFocus={{ scale: 1.1 }} tabIndex={0}>
Keyboard accessible
</motion.button>For complete accessibility guide (ARIA patterns, screen readers, AnimatePresence workaround, testing), load references/accessibility-guide.md.
---
Common Patterns
Modal Dialog (AnimatePresence + backdrop):
<AnimatePresence>
{isOpen && (
<motion.dialog exit={{ opacity: 0 }}>Content</motion.dialog>
)}
</AnimatePresence>Accordion (height animation):
<motion.div animate={{ height: isOpen ? "auto" : 0 }}>
Content
</motion.div>For 15+ production patterns (carousel, tabs, scroll reveal, parallax, notifications), load references/common-patterns.md.
---
Known Issues & Solutions
Issue 1: AnimatePresence Exit Not Working (MOST COMMON)
Symptom: Components disappear instantly without exit animation.
Solution: AnimatePresence must stay mounted, wrap the conditional (not be wrapped by it):
// ❌ Wrong
{isVisible && <AnimatePresence><motion.div>Content</motion.div></AnimatePresence>}
// ✅ Correct
<AnimatePresence>
{isVisible && <motion.div key="unique">Content</motion.div>}
</AnimatePresence>Issue 2: Next.js "use client" Missing
Symptom: Build fails with "motion is not defined" or SSR errors.
Solution: Add "use client" directive:
"use client"
import { motion } from "motion/react"Issue 3: Tailwind Transitions Conflict
Symptom: Animations stutter or don't work.
Solution: Remove transition-* classes (Motion overrides CSS transitions):
// ❌ Wrong: <motion.div className="transition-all" animate={{ x: 100 }} />
// ✅ Correct: <motion.div animate={{ x: 100 }} />Issue 4: Cloudflare Workers Build Errors
Symptom: Wrangler build fails when using motion package.
Solution: Use framer-motion v12.23.24 instead (GitHub issue #2918):
bun add framer-motion # Same API, works with WorkersIssue 5: Large List Performance
Symptom: 50-100+ animated items cause severe slowdown.
Solution: Use virtualization (react-window, react-virtuoso).
For 5+ additional issues (layoutScroll, layoutRoot, AnimatePresence + layoutId), load references/nextjs-integration.md or references/core-concepts-deep-dive.md.
---
When to Load References
Claude should load these references based on user needs:
Load references/core-concepts-deep-dive.md when:
- User asks about variants orchestration (staggerChildren, delayChildren, dynamic variants)
- User needs advanced layout animations (layoutId shared transitions, LayoutGroup)
- User wants scroll-linked animations (useScroll offsets, useTransform easing, parallax layers)
- User needs complex drag patterns (momentum, elastic, event handlers, constraints)
- User asks about spring physics tuning (mass parameter, visualizer, custom presets)
Load references/performance-optimization.md when:
- User wants to reduce bundle size below 4.6 KB (useAnimate mini, LazyMotion comparison)
- User mentions "app is slow", "janky animations", "laggy", or "performance issues"
- User has 50+ animated items in a list (virtualization needed)
- User needs memory profiling or production benchmarks
Load references/nextjs-integration.md when:
- User is building with Next.js (App Router or Pages Router)
- User encounters SSR errors, "use client" errors, or hydration issues
- User asks about route transitions or page navigation animations
- User needs Next.js-specific workarounds (Reorder component, AnimatePresence soft navigation)
Load references/accessibility-guide.md when:
- User asks about "prefers-reduced-motion" or accessibility compliance
- User needs ARIA integration patterns (roles, labels, announcements)
- User wants screen reader compatibility
- User mentions accessibility audits or WCAG compliance
- User asks about AnimatePresence reducedMotion workaround (known issue #1567)
Load references/common-patterns.md when:
- User asks for specific UI patterns (modal, accordion, carousel, tabs, dropdown, toast, etc.)
- User needs copy-paste code examples for production use
- User wants to see 15+ real-world animation patterns
Load references/motion-vs-auto-animate.md when:
- User is deciding between Motion and AutoAnimate libraries
- User mentions "simple list animations" or "bundle size concerns"
- User asks "which animation library should I use?" or "is Motion overkill?"
- User needs feature comparison or decision matrix
---
Templates
This skill includes 5 production-ready templates in the templates/ directory:
1. motion-vite-basic.tsx - Basic Vite + React + TypeScript setup with common animations 2. motion-nextjs-client.tsx - Next.js App Router pattern with client component wrapper 3. scroll-parallax.tsx - Scroll animations, parallax, and viewport triggers 4. ui-components.tsx - Modal, accordion, carousel, tabs with shared underline 5. layout-transitions.tsx - FLIP layout animations and shared element transitions
Copy templates into your project and customize as needed.
---
References
This skill includes 4 comprehensive reference guides:
- motion-vs-auto-animate.md - Decision guide: when to use Motion vs AutoAnimate
- performance-optimization.md - Bundle size, LazyMotion, virtualization, hardware acceleration
- nextjs-integration.md - App Router vs Pages Router, "use client", known issues
- common-patterns.md - Top 15 patterns with full code examples
See references/ directory for detailed guides.
---
Scripts
This skill includes 2 automation scripts:
- init-motion.sh - One-command setup with framework detection (Vite, Next.js, Cloudflare Workers)
- optimize-bundle.sh - Convert existing Motion code to LazyMotion for smaller bundle
See scripts/ directory for automation tools.
---
Official Documentation
- Official Site: https://motion.dev
- GitHub: https://github.com/motiondivision/motion (30,200+ stars)
- Examples: https://motion.dev/examples (300+ examples)
Related Skills: auto-animate (simple lists), tailwind-v4-shadcn (styling), nextjs (App Router), cloudflare-worker-base
Motion vs AutoAnimate: Load references/motion-vs-auto-animate.md for detailed comparison.
---
Token Efficiency Metrics
Token Savings: ~83% (30k → 5k tokens) | Error Prevention: 100% (29+ errors) | Time Savings: ~85% (2-3 hrs → 20-30 min)
---
Package Versions (Verified 2025-11-07)
| Package | Version | Status |
|---|---|---|
| motion | 12.23.24 | ✅ Latest stable |
| framer-motion | 12.23.24 | ✅ Alternative for Cloudflare |
| react | 19.2.0 | ✅ Latest stable |
| vite | 6.0.0 | ✅ Latest stable |
---
Contributing
Found an issue or have a suggestion?
- Open an issue: https://github.com/secondsky/claude-skills/issues
- See templates and references for detailed examples
---
Production Tested: ✅ React 19 + Next.js 15 + Vite 6 + Tailwind v4 Token Savings: ~83% Error Prevention: 100% (29+ documented errors prevented) Bundle Size: 2.3 KB (mini) - 34 KB (full), optimizable to 4.6 KB with LazyMotion Accessibility: MotionConfig reducedMotion support Ready to use! Install with ./scripts/install-skill.sh motion
Motion Accessibility Guide
Complete guide to making Motion animations accessible for all users, including those with motion sensitivities, keyboard-only navigation, and screen readers.
---
Table of Contents
1. Respecting prefers-reduced-motion 2. Keyboard Navigation Support 3. ARIA Integration 4. Testing Accessibility
---
Respecting prefers-reduced-motion
What is prefers-reduced-motion?
Users can enable "Reduce Motion" in their operating system settings to indicate they prefer minimal animation. This setting helps users with:
- Vestibular disorders (motion sickness from animations)
- Attention disorders (distraction from movement)
- Epilepsy (seizure triggers)
- Personal preference
How to Enable (for testing)
macOS: 1. System Settings → Accessibility → Display 2. Enable "Reduce motion"
Windows: 1. Settings → Ease of Access → Display 2. Enable "Show animations" → OFF
iOS: 1. Settings → Accessibility → Motion 2. Enable "Reduce Motion"
Android 9+: 1. Settings → Accessibility 2. Enable "Remove animations"
Implementation with MotionConfig
The recommended approach for respecting user preferences:
import { MotionConfig } from "motion/react"
function App() {
return (
<MotionConfig reducedMotion="user">
{/* All Motion components respect OS setting */}
<YourApp />
</MotionConfig>
)
}How it works:
reducedMotion="user": Respects OS setting (default behavior)reducedMotion="always": Force instant transitions (no animations)reducedMotion="never": Ignore OS setting (always animate)
What happens when enabled:
- All transitions become instant (
duration: 0) - Spring animations skip to final state
- Layout animations still work but happen immediately
- Enter/exit animations complete instantly
Manual Detection (for fine-grained control)
When MotionConfig isn't sufficient:
import { useReducedMotion } from "motion/react"
function Component() {
const prefersReducedMotion = useReducedMotion()
return (
<motion.div
animate={{ x: 100 }}
transition={{
duration: prefersReducedMotion ? 0 : 0.5,
type: prefersReducedMotion ? "tween" : "spring"
}}
/>
)
}Alternative (vanilla JavaScript):
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches
<motion.div
animate={{ opacity: 1 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
/>AnimatePresence and reducedMotion
Fixed: Motion v12.4.7 (February-March 2025) fixed the issue where AnimatePresence ignored reducedMotion settings.
If you're experiencing issues with AnimatePresence not respecting reduced motion preferences, ensure you're using Motion v12.4.7 or later:
# npm
npm install motion@^12.4.7
# yarn
yarn add motion@^12.4.7
# bun
bun add motion@^12.4.7Creating a Reusable Hook
// hooks/useMotionConfig.ts
import { useReducedMotion } from "motion/react"
export function useMotionConfig() {
const prefersReducedMotion = useReducedMotion()
return {
transition: {
duration: prefersReducedMotion ? 0 : 0.3,
type: prefersReducedMotion ? "tween" : "spring",
},
initial: (withMotion: Record<string, any>) =>
prefersReducedMotion ? {} : withMotion,
exit: (withMotion: Record<string, any>) =>
prefersReducedMotion ? {} : withMotion,
}
}
// Usage
function Component() {
const config = useMotionConfig()
return (
<motion.div
initial={config.initial({ opacity: 0, y: 20 })}
animate={{ opacity: 1, y: 0 }}
exit={config.exit({ opacity: 0, y: 20 })}
transition={config.transition}
/>
)
}Best Practices
Do:
- ✅ Wrap app in MotionConfig with
reducedMotion="user" - ✅ Manually check for AnimatePresence components
- ✅ Test with reduced motion enabled
- ✅ Provide instant alternatives (not just removing animations)
- ✅ Keep content readable even without animations
Don't:
- ❌ Ignore reduced motion preference
- ❌ Assume all users want animations
- ❌ Hide critical content behind animations
- ❌ Use animations for essential functionality
- ❌ Force animations when user prefers reduced motion
---
Keyboard Navigation Support
Focus States with whileFocus
Motion provides first-class keyboard support through whileFocus:
<motion.button
whileFocus={{ scale: 1.1, boxShadow: "0 0 0 3px rgba(66, 153, 225, 0.5)" }}
whileTap={{ scale: 0.95 }}
tabIndex={0}
>
Keyboard accessible button
</motion.button>How it works:
whileFocustriggers when element receives keyboard focus (Tab key)- Works with screen readers
- Respects browser's native focus ring
- Can be combined with hover/tap states
Tab Order and tabIndex
Ensure logical tab order for animated elements:
function NavigationMenu() {
const [isOpen, setIsOpen] = useState(false)
return (
<nav>
<motion.button
onClick={() => setIsOpen(!isOpen)}
tabIndex={0} // Tab order: 1st
>
Menu
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.ul
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<li><a href="/" tabIndex={1}>Home</a></li>
<li><a href="/about" tabIndex={2}>About</a></li>
<li><a href="/contact" tabIndex={3}>Contact</a></li>
</motion.ul>
)}
</AnimatePresence>
</nav>
)
}Best practices:
- Use sequential
tabIndexvalues (0, 1, 2, ...) - Never use
tabIndex={-1}unless element should be unfocusable - Test tab order with keyboard only
- Ensure all interactive elements are reachable
Keyboard Shortcuts for Gestures
Provide keyboard alternatives for drag operations:
function DraggableCard() {
const [position, setPosition] = useState({ x: 0, y: 0 })
const handleKeyDown = (e: KeyboardEvent) => {
const step = 10
switch (e.key) {
case "ArrowLeft":
setPosition(prev => ({ ...prev, x: prev.x - step }))
break
case "ArrowRight":
setPosition(prev => ({ ...prev, x: prev.x + step }))
break
case "ArrowUp":
setPosition(prev => ({ ...prev, y: prev.y - step }))
break
case "ArrowDown":
setPosition(prev => ({ ...prev, y: prev.y + step }))
break
}
}
return (
<motion.div
drag
dragMomentum={false}
animate={position}
onKeyDown={handleKeyDown}
tabIndex={0}
role="button"
aria-label="Draggable card. Use arrow keys to move."
>
Drag with mouse or move with arrow keys
</motion.div>
)
}Focus Management in Modals
Trap focus within modal dialogs:
import { useRef, useEffect } from "react"
import { motion, AnimatePresence } from "motion/react"
function AccessibleModal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
// Store currently focused element
previousFocusRef.current = document.activeElement as HTMLElement
// Focus first focusable element in modal
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
if (focusable && focusable.length > 0) {
(focusable[0] as HTMLElement).focus()
}
} else {
// Restore focus when modal closes
previousFocusRef.current?.focus()
}
}, [isOpen])
return (
<AnimatePresence>
{isOpen && (
<motion.div
ref={modalRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
role="dialog"
aria-modal="true"
onKeyDown={(e) => {
// Close on Escape
if (e.key === "Escape") onClose()
}}
>
{children}
</motion.div>
)}
</AnimatePresence>
)
}---
ARIA Integration
ARIA Labels for Animated Elements
Help screen readers understand animated UI:
// Loading spinner
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
role="status"
aria-label="Loading"
aria-live="polite"
>
<span className="sr-only">Loading content...</span>
</motion.div>
// Expandable section
function Accordion({ title, children }) {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<motion.button
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls="accordion-content"
>
{title}
</motion.button>
<motion.div
id="accordion-content"
initial={false}
animate={{ height: isOpen ? "auto" : 0 }}
style={{ overflow: "hidden" }}
role="region"
aria-hidden={!isOpen}
>
{children}
</motion.div>
</div>
)
}Dynamic ARIA Announcements
Announce state changes to screen readers:
import { useState, useEffect } from "react"
function SearchResults({ results }) {
const [announcement, setAnnouncement] = useState("")
useEffect(() => {
setAnnouncement(`${results.length} results found`)
// Clear announcement after delay
const timer = setTimeout(() => setAnnouncement(""), 1000)
return () => clearTimeout(timer)
}, [results])
return (
<>
{/* Screen reader announcement */}
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
{announcement}
</div>
{/* Animated results */}
<motion.ul layout>
<AnimatePresence>
{results.map(result => (
<motion.li
key={result.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -100 }}
>
{result.name}
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</>
)
}ARIA Roles for Common Patterns
// Tabs
<motion.div role="tablist">
<motion.button role="tab" aria-selected={isActive}>
Tab 1
</motion.button>
<motion.button role="tab" aria-selected={!isActive}>
Tab 2
</motion.button>
</motion.div>
// Carousel
<motion.div role="region" aria-label="Image carousel">
<motion.div role="group" aria-roledescription="slide">
<img alt="Product 1" />
</motion.div>
</motion.div>
// Alert/notification
<motion.div
role="alert"
aria-live="assertive"
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
>
Error: Please try again
</motion.div>---
Testing Accessibility
Manual Testing Checklist
Keyboard Navigation:
- [ ] All interactive elements reachable via Tab key
- [ ] Logical tab order (left to right, top to bottom)
- [ ] Focus visible on all elements
- [ ] No keyboard traps (can tab out of all components)
- [ ] Escape key closes modals/dropdowns
- [ ] Enter/Space activates buttons
Screen Reader:
- [ ] ARIA labels present and descriptive
- [ ] State changes announced (loading, errors, success)
- [ ] Animated content has text alternatives
- [ ] Modal focus trapped and restored correctly
- [ ] Lists/grids have proper roles
Reduced Motion:
- [ ] Enable OS reduced motion setting
- [ ] Animations become instant
- [ ] Content still accessible
- [ ] No functionality lost
- [ ] AnimatePresence components manually handled
Automated Testing Tools
Install axe-core for Jest/Vitest:
bun add -d @axe-core/react
# or: npm install --save-dev @axe-core/reactUsage:
import { axe, toHaveNoViolations } from 'jest-axe'
import { render } from '@testing-library/react'
expect.extend(toHaveNoViolations)
test('Modal is accessible', async () => {
const { container } = render(<Modal isOpen={true}>Content</Modal>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})Install Lighthouse CI:
npm install -g @lhci/cliRun accessibility audit:
lhci autorun --collect.settings.onlyCategories=accessibilityBrowser DevTools
Chrome DevTools: 1. Open DevTools → Lighthouse tab 2. Select "Accessibility" category 3. Click "Generate report" 4. Fix any issues found
Firefox Accessibility Inspector: 1. Open DevTools → Accessibility tab 2. Click "Check for Issues" 3. Review violations and warnings
Safari Accessibility Audit: 1. Develop → Show Web Inspector 2. Audits tab → Run audit 3. Review accessibility issues
Real User Testing
Screen Reader Testing:
- macOS: VoiceOver (Cmd+F5)
- Windows: NVDA (free) or JAWS
- iOS: VoiceOver (Settings → Accessibility)
- Android: TalkBack (Settings → Accessibility)
Test scenarios: 1. Navigate entire page with screen reader only 2. Complete primary user flows 3. Ensure announcements are clear and timely 4. Verify form validation messages are announced
---
Accessibility Patterns for Common Use Cases
Accessible Loading States
function LoadingSpinner({ label = "Loading" }) {
return (
<motion.div
role="status"
aria-label={label}
aria-live="polite"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
{/* Hidden text for screen readers */}
<span className="sr-only">{label}...</span>
{/* Visual spinner */}
<svg>...</svg>
</motion.div>
)
}Accessible Drag and Drop
function AccessibleDragList({ items, onReorder }) {
const [focused, setFocused] = useState<string | null>(null)
const moveItem = (id: string, direction: "up" | "down") => {
const index = items.findIndex(item => item.id === id)
const newIndex = direction === "up" ? index - 1 : index + 1
if (newIndex >= 0 && newIndex < items.length) {
const newItems = [...items]
const [removed] = newItems.splice(index, 1)
newItems.splice(newIndex, 0, removed)
onReorder(newItems)
// Announce change to screen reader
announceChange(`Moved ${removed.name} ${direction}`)
}
}
return (
<ul role="list" aria-label="Reorderable list">
{items.map(item => (
<motion.li
key={item.id}
layout
drag="y"
tabIndex={0}
role="listitem"
aria-grabbed={focused === item.id}
onKeyDown={(e) => {
if (e.key === "ArrowUp") moveItem(item.id, "up")
if (e.key === "ArrowDown") moveItem(item.id, "down")
if (e.key === " ") setFocused(focused === item.id ? null : item.id)
}}
>
{item.name}
<span className="sr-only">
Press space to grab, arrow keys to move
</span>
</motion.li>
))}
</ul>
)
}---
Resources
Official Documentation
- WCAG Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
- MDN Accessibility: https://developer.mozilla.org/en-US/docs/Web/Accessibility
- A11y Project: https://www.a11yproject.com/
Testing Tools
- axe DevTools: https://www.deque.com/axe/devtools/
- WAVE: https://wave.webaim.org/
- Pa11y: https://pa11y.org/
Screen Readers
- NVDA (Windows, free): https://www.nvaccess.org/
- VoiceOver (macOS/iOS, built-in)
- JAWS (Windows, paid): https://www.freedomscientific.com/products/software/jaws/
---
Related Files
- SKILL.md - Basic accessibility examples (MotionConfig, keyboard support)
- common-patterns.md - Accessible pattern implementations
- core-concepts-deep-dive.md - Advanced animation techniques
- nextjs-integration.md - Framework-specific considerations
---
Last Updated: 2025-11-28 WCAG Compliance: WCAG 2.1 Level AA Production Tested: React 19 + Next.js 15
Motion Common Patterns - Quick Reference
Production-tested animation patterns with code examples. Copy-paste ready.
---
1. Modal Dialog
import { motion, AnimatePresence } from "motion/react"
<AnimatePresence>
{isOpen && (
<>
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
<motion.dialog
key="dialog"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="fixed inset-0 m-auto w-96 bg-white rounded-lg shadow-xl z-50"
>
Content
</motion.dialog>
</>
)}
</AnimatePresence>---
2. Accordion
<motion.div
animate={{ height: isOpen ? "auto" : 0 }}
style={{ overflow: "hidden" }}
transition={{ duration: 0.3 }}
>
<div className="p-4">Content</div>
</motion.div>---
3. Tabs with Shared Underline
<div className="flex gap-4 border-b">
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{active === tab.id && (
<motion.div
layoutId="underline"
className="absolute bottom-0 h-0.5 bg-blue-600"
/>
)}
</button>
))}
</div>---
4. Staggered List
const container = {
hidden: {},
show: { transition: { staggerChildren: 0.1 } }
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map(item => (
<motion.li key={item.id} variants={item}>
{item.text}
</motion.li>
))}
</motion.ul>---
5. Parallax Hero
const { scrollY } = useScroll()
const y = useTransform(scrollY, [0, 1000], [0, -300])
<motion.div style={{ y }}>
<img src="/background.jpg" />
</motion.div>---
6. Scroll Progress Bar
const { scrollYProgress } = useScroll()
<motion.div
style={{ scaleX: scrollYProgress }}
className="fixed top-0 h-1 bg-blue-600 origin-left"
/>---
7. Fade In on Scroll
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
>
Content
</motion.div>---
8. Drag to Reorder
<motion.div
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.1}
whileDrag={{ scale: 1.05 }}
>
Drag me
</motion.div>---
9. Card Expand
<motion.div layout onClick={toggle} className={isExpanded ? "w-full" : "w-64"}>
{isExpanded && <p>Extra content</p>}
</motion.div>---
10. Shared Element Transition
// Grid view
<motion.div layoutId={card.id} onClick={() => expand(card)}>
Preview
</motion.div>
// Detail view
<motion.div layoutId={card.id}>
Full details
</motion.div>---
11. Carousel
<motion.div
drag="x"
dragConstraints={{ left: -width, right: 0 }}
className="flex"
>
{images.map(img => <img key={img.id} src={img.url} />)}
</motion.div>---
12. Hover & Tap Button
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
Click me
</motion.button>---
13. Toast Notification
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, x: 300 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 300 }}
className="fixed top-4 right-4 bg-blue-600 text-white p-4 rounded"
>
Message
</motion.div>
)}
</AnimatePresence>---
14. SVG Line Drawing
<motion.path
d="M 0 50 Q 50 0 100 50"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2 }}
stroke="black"
strokeWidth={2}
fill="none"
/>---
15. Spring Physics
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300,
damping: 10
}}
/>---
See templates/ for full component examples.
Motion Core Concepts - Deep Dive
This guide covers advanced Motion concepts for complex animation scenarios. For basic usage, see the main SKILL.md file.
---
Table of Contents
1. Advanced Variants Orchestration 2. Advanced Layout Animations (FLIP) 3. Advanced Scroll Animations 4. Advanced Gesture Controls 5. Spring Physics Tuning
---
Advanced Variants Orchestration
Variants enable sophisticated animation choreography across component trees.
Staggered Children Animations
Control the timing between each child animation:
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1, // 100ms delay between each child
delayChildren: 0.3, // Wait 300ms before starting children
staggerDirection: 1, // 1 = forward, -1 = reverse
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item, i) => (
<motion.li key={item.id} variants={item}>
{/* Animates 100ms after previous sibling */}
{item.text}
</motion.li>
))}
</motion.ul>Dynamic Variants with Functions
Create variants that respond to component state:
const box = {
start: { scale: 1 },
end: (custom) => ({
scale: custom.scale,
rotate: custom.rotate,
transition: { duration: custom.duration }
})
}
<motion.div
variants={box}
initial="start"
animate="end"
custom={{ scale: 2, rotate: 45, duration: 0.5 }}
/>Nested Variant Propagation
Variants automatically propagate through component hierarchy:
const list = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
when: "beforeChildren", // Animate parent before children
staggerChildren: 0.1,
},
},
}
const item = {
hidden: { x: -10, opacity: 0 },
visible: {
x: 0,
opacity: 1,
},
}
function List({ items }) {
return (
<motion.ul variants={list} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={item}>
{/* Child inherits parent's variant state */}
{item.text}
</motion.li>
))}
</motion.ul>
)
}Orchestration Options
Control the relationship between parent and children animations:
const parent = {
animate: {
opacity: 1,
transition: {
// Control animation order
when: "beforeChildren", // Parent animates before children
// when: "afterChildren", // Parent animates after children
// Stagger children
staggerChildren: 0.1, // Delay between each child
delayChildren: 0.2, // Initial delay before first child
staggerDirection: 1, // 1 = first to last, -1 = last to first
}
}
}Use Cases:
- Staggered list reveals (menu items, search results)
- Sequential card animations
- Cascading hover effects
- Coordinated multi-element transitions
---
Advanced Layout Animations (FLIP)
FLIP (First, Last, Invert, Play) animations automatically handle complex layout changes.
Shared Element Transitions with layoutId
Connect separate elements for smooth morphing:
function Gallery() {
const [selectedId, setSelectedId] = useState(null)
return (
<>
<div className="grid grid-cols-3 gap-4">
{items.map(item => (
<motion.div
key={item.id}
layoutId={item.id} // Connects this to detail view
onClick={() => setSelectedId(item.id)}
>
<img src={item.thumbnail} />
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={selectedId} // Same layoutId = shared transition
onClick={() => setSelectedId(null)}
>
<img src={items.find(i => i.id === selectedId).fullSize} />
</motion.div>
)}
</AnimatePresence>
</>
)
}How it works: 1. Motion identifies elements with matching layoutId 2. Calculates position/size difference 3. Animates transform to bridge the gap 4. Element visually "morphs" between states
Layout Scroll Fix
When layout animations happen inside scrollable containers:
<motion.div
layoutScroll // Fix: Accounts for scroll offset
className="overflow-auto h-96"
>
{items.map(item => (
<motion.div key={item.id} layout>
{/* Layout animations work correctly even when scrolled */}
{item.content}
</motion.div>
))}
</motion.div>Problem without layoutScroll: Removing items from scrolled container causes incomplete transitions.
Solution: layoutScroll prop accounts for scroll offset when calculating FLIP positions.
Layout Root for Fixed Elements
Fixed/absolute positioned elements need special handling:
<motion.div
layoutRoot // Creates new layout context
className="fixed top-0 left-0 w-full"
>
<motion.div layout>
{/* Layout animations work correctly in fixed container */}
<Navigation />
</motion.div>
</motion.div>Use Cases:
- Fixed headers with layout changes
- Sticky sidebars
- Modal overlays with animated content
- Floating action buttons
Layout Groups
Synchronize layout animations across multiple components:
import { LayoutGroup } from "motion/react"
<LayoutGroup>
<motion.div layout>Column 1</motion.div>
<motion.div layout>Column 2</motion.div>
<motion.div layout>Column 3</motion.div>
</LayoutGroup>What it does: All siblings in LayoutGroup coordinate their layout animations to feel connected.
Example: Grid reordering, responsive layout shifts, tab panels
---
Advanced Scroll Animations
useScroll Hook - Fine-Grained Control
Access scroll progress for custom animations:
import { useScroll, useTransform, motion } from "motion/react"
import { useRef } from "react"
function ScrollSection() {
const ref = useRef(null)
// Track scroll progress of this element (not whole page)
const { scrollYProgress } = useScroll({
target: ref, // Element to track
offset: ["start end", "end start"] // When to start/end tracking
})
// Transform scroll progress to different values
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8])
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360])
return (
<motion.div
ref={ref}
style={{ opacity, scale, rotate }}
>
{/* Fades in, scales up, rotates as user scrolls */}
</motion.div>
)
}Scroll Offset Explained
Control when scroll tracking starts/ends:
const { scrollYProgress } = useScroll({
target: ref,
offset: [
"start end", // Start tracking when element top hits viewport bottom
"end start" // Stop tracking when element bottom hits viewport top
]
})Offset syntax: ["start position", "end position"]
Positions:
start= top of elementcenter= middle of elementend= bottom of element- Add viewport position:
start end= element start to viewport end
Examples:
// Element visible in viewport
offset: ["start end", "end start"]
// Element centered in viewport
offset: ["center center", "center center"]
// Start 100px before element enters
offset: ["start calc(end + 100px)", "end start"]useTransform - Value Mapping
Convert scroll progress to any value range:
import { useScroll, useTransform } from "motion/react"
const { scrollYProgress } = useScroll()
// Map 0-1 scroll to pixel values
const y = useTransform(scrollYProgress, [0, 1], [0, -500])
// Map to degrees
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360])
// Map to colors (requires motion/react-client)
const backgroundColor = useTransform(
scrollYProgress,
[0, 0.5, 1],
["#ff0000", "#00ff00", "#0000ff"]
)
// Custom easing
const scaleWithEasing = useTransform(
scrollYProgress,
[0, 0.5, 1],
[1, 1.5, 1],
{ ease: "easeInOut" }
)Parallax Layers
Create depth with different scroll speeds:
function ParallaxScene() {
const { scrollYProgress } = useScroll()
// Background moves slowest (depth)
const backgroundY = useTransform(scrollYProgress, [0, 1], [0, -200])
// Midground moves medium speed
const midgroundY = useTransform(scrollYProgress, [0, 1], [0, -400])
// Foreground moves fastest (closest)
const foregroundY = useTransform(scrollYProgress, [0, 1], [0, -600])
return (
<div className="relative h-[200vh]">
<motion.div
className="fixed inset-0"
style={{ y: backgroundY }}
>
<img src="/mountains.jpg" />
</motion.div>
<motion.div
className="fixed inset-0"
style={{ y: midgroundY }}
>
<img src="/trees.png" />
</motion.div>
<motion.div
className="fixed inset-0"
style={{ y: foregroundY }}
>
<img src="/foreground.png" />
</motion.div>
</div>
)
}Scroll-Linked Progress Bars
function ReadingProgressBar() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}---
Advanced Gesture Controls
Drag Constraints - Dynamic Boundaries
Constrain dragging to specific bounds:
function ConstrainedDrag() {
const constraintsRef = useRef(null)
return (
<div ref={constraintsRef} className="w-96 h-96 border">
<motion.div
drag
dragConstraints={constraintsRef} // Can't drag outside parent
dragElastic={0.1} // Slight resistance at edges
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
>
Drag me (constrained to parent)
</motion.div>
</div>
)
}Constraint options:
// Pixel-based constraints
dragConstraints={{ top: -50, right: 50, bottom: 50, left: -50 }}
// Ref-based (constrain to parent element)
dragConstraints={constraintsRef}
// Elastic resistance (0 = rigid, 1 = elastic)
dragElastic={0.2}Drag Momentum
Control how elements behave when released:
<motion.div
drag="x"
dragMomentum={true} // Continue moving after release
dragTransition={{
power: 0.2, // Higher = longer momentum
timeConstant: 200, // How long momentum lasts (ms)
modifyTarget: (target) => {
// Snap to nearest 100px
return Math.round(target / 100) * 100
}
}}
/>Use Cases:
- Carousel swiping with snap points
- Physics-based card throwing
- Momentum scrolling
- Flick-to-dismiss gestures
Drag Event Handlers
React to drag lifecycle:
<motion.div
drag
onDragStart={(event, info) => {
console.log("Started dragging", info.point)
}}
onDrag={(event, info) => {
console.log("Dragging", info.offset, info.velocity)
}}
onDragEnd={(event, info) => {
console.log("Released", info.offset, info.velocity)
// Snap back if dragged too little
if (Math.abs(info.offset.x) < 100) {
animate({ x: 0 })
}
}}
/>Event info object:
{
point: { x: number, y: number }, // Pointer position
delta: { x: number, y: number }, // Movement since last event
offset: { x: number, y: number }, // Total movement from start
velocity: { x: number, y: number } // Current velocity
}Direction-Locked Dragging
Lock to horizontal or vertical axis:
// Horizontal only
<motion.div drag="x">Swipe left/right</motion.div>
// Vertical only
<motion.div drag="y">Swipe up/down</motion.div>
// Both directions
<motion.div drag>Free movement</motion.div>
// Conditional locking
<motion.div drag={isUnlocked ? true : "x"}>
Locked to X until unlocked
</motion.div>---
Spring Physics Tuning
Understanding Spring Parameters
Springs create natural, physics-based motion:
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 100, // How hard spring pulls (0-500+)
damping: 10, // Resistance to oscillation (0-100)
mass: 1, // How heavy element feels (0.1-10)
}}
/>Parameter effects:
Stiffness (spring strength):
- Low (50-100): Slow, smooth movement
- Medium (100-300): Balanced, natural
- High (300-500): Fast, snappy
Damping (oscillation control):
- Low (5-10): Bouncy, multiple oscillations
- Medium (10-20): Slight overshoot
- High (20-50): No overshoot, smooth stop
- Very high (50-100): Heavy, sluggish
Mass (weight):
- Light (0.1-0.5): Quick, reactive
- Normal (1): Default weight
- Heavy (2-10): Slow, lethargic
Common Presets
// Bouncy (button click, playful UI)
transition: {
type: "spring",
stiffness: 300,
damping: 10,
mass: 0.5,
}
// Smooth (modal open, drawer slide)
transition: {
type: "spring",
stiffness: 100,
damping: 20,
mass: 1,
}
// Snappy (toggle switch, quick feedback)
transition: {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.5,
}
// Heavy (large elements, draggable cards)
transition: {
type: "spring",
stiffness: 100,
damping: 15,
mass: 2,
}
// Elastic (rubber band effect)
transition: {
type: "spring",
stiffness: 200,
damping: 5,
mass: 1,
}Duration vs Stiffness
Springs don't use duration by default (physics-based). To set duration:
// Duration-based spring (less natural)
transition: {
type: "spring",
duration: 0.5,
bounce: 0.3, // 0 = no bounce, 1 = very bouncy
}
// Physics-based spring (more natural)
transition: {
type: "spring",
stiffness: 100,
damping: 10,
}Recommendation: Use stiffness/damping for natural feel, duration/bounce for precise timing.
Visualizing Spring Physics
Test spring parameters interactively:
Official Spring Visualizer: https://motion.dev/tools/spring
// Copy values from visualizer
transition: {
type: "spring",
stiffness: 260,
damping: 20,
mass: 1.2,
}When to Use Springs
Use springs for:
- Interactive gestures (drag, tap, hover)
- Natural-feeling UI (modals, dropdowns, drawers)
- Playful animations (button clicks, micro-interactions)
- Responsive layouts (grid reordering, expand/collapse)
Don't use springs for:
- Precise timing requirements (use duration instead)
- Synchronized choreography (use duration for predictability)
- Loading states (use duration for consistent timing)
---
Related Files
- SKILL.md - Basic concepts and quick start
- common-patterns.md - Production-ready examples using these concepts
- nextjs-integration.md - Framework-specific advanced usage
- performance-optimization.md - Optimizing complex animations
---
Last Updated: 2025-11-28 Production Tested: React 19 + Next.js 15 + Vite 6
Motion vs AutoAnimate - Decision Guide
This document helps you choose between Motion (Framer Motion) and AutoAnimate for your animation needs.
---
TL;DR - Quick Decision
Use AutoAnimate when:
- ✅ Animating list add/remove/sort operations
- ✅ Simple accordion expand/collapse
- ✅ Toast notifications fade in/out
- ✅ Form validation error messages appearing/disappearing
- ✅ Bundle size is critical (3.28 KB)
- ✅ Want zero configuration
Use Motion when:
- ✅ Need gesture controls (drag, hover, tap with fine control)
- ✅ Need scroll-based animations or parallax
- ✅ Need layout/shared element transitions
- ✅ Need SVG path morphing or line drawing
- ✅ Need spring physics customization
- ✅ Need complex orchestrated animations
Rule of Thumb: AutoAnimate for 90% of cases, Motion for 10%
---
Bundle Size Comparison
| Package | Minified + Gzipped | Use Case |
|---|---|---|
| AutoAnimate | 3.28 KB | Simple list animations |
| Motion useAnimate mini | 2.3 KB | Smallest React animation |
| Motion useAnimate hybrid | 17 KB | Imperative animations |
| Motion with LazyMotion | 4.6 KB | Optimized declarative |
| Motion full component | 34 KB | Full feature set |
Winner for bundle size: Motion useAnimate mini (2.3 KB) or AutoAnimate (3.28 KB)
Winner for features: Motion full (34 KB)
Sweet spot: LazyMotion (4.6 KB) for most Motion use cases
---
API Complexity Comparison
AutoAnimate (Zero Config)
import { useAutoAnimate } from '@formkit/auto-animate/react'
const [parent] = useAutoAnimate()
return (
<ul ref={parent}>
{items.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
)Lines of code: 3 Configuration: 0 Learning curve: Minutes
Motion (Declarative)
import { motion } from 'motion/react'
return (
<ul>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
layout
>
{item.text}
</motion.li>
))}
</ul>
)Lines of code: 12 Configuration: 4 props Learning curve: Hours
Winner for simplicity: AutoAnimate
Winner for control: Motion
---
Feature Comparison
| Feature | AutoAnimate | Motion |
|---|---|---|
| List add/remove | ✅ Automatic | ✅ Manual setup |
| List reorder | ✅ Automatic | ✅ Manual setup |
| Accordion | ✅ Automatic | ✅ Manual setup |
| Drag gestures | ❌ Not supported | ✅ Full control |
| Hover states | ❌ Not supported | ✅ whileHover prop |
| Tap states | ❌ Not supported | ✅ whileTap prop |
| Scroll animations | ❌ Not supported | ✅ whileInView, useScroll |
| Parallax | ❌ Not supported | ✅ useTransform |
| Layout animations | ❌ Not supported | ✅ layout prop, FLIP |
| Shared elements | ❌ Not supported | ✅ layoutId |
| SVG animations | ❌ Not supported | ✅ path, line drawing |
| Spring physics | ❌ Not customizable | ✅ Full control |
| Variants/orchestration | ❌ Not supported | ✅ Stagger, delay, sequence |
| Exit animations | ✅ Automatic | ✅ AnimatePresence |
| TypeScript | ✅ Native support | ✅ Native support |
| SSR/Next.js | ✅ Full support | ✅ Full support (use client) |
| Cloudflare Workers | ✅ Full support | ⚠️ Use framer-motion instead |
| Accessibility | ✅ Auto prefers-reduced-motion | ✅ Manual MotionConfig |
---
Use Case Breakdown
Simple List Animations (90% of use cases)
Scenario: Todo list, shopping cart, search results, notification list
AutoAnimate:
const [parent] = useAutoAnimate()
return <ul ref={parent}>{items.map(...)}</ul>Motion:
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
layout
>
{item.text}
</motion.li>
))}
</AnimatePresence>Recommendation: AutoAnimate (simpler, smaller)
---
Accordion Components
Scenario: FAQ, collapsible sections, navigation menus
AutoAnimate:
const [parent] = useAutoAnimate()
return (
<div ref={parent}>
{isOpen && <div>Content</div>}
</div>
)Motion:
<motion.div
animate={{ height: isOpen ? "auto" : 0 }}
style={{ overflow: "hidden" }}
>
<div>Content</div>
</motion.div>Recommendation: AutoAnimate for simple accordions, Motion if you need:
- Custom spring physics
- Staggered child animations
- Scroll-triggered expand/collapse
---
Modal Dialogs
Scenario: Popup dialogs, overlays, lightboxes
AutoAnimate:
- Not ideal (doesn't handle backdrop animations well)
Motion:
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="backdrop"
/>
<motion.dialog
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
>
Content
</motion.dialog>
</>
)}
</AnimatePresence>Recommendation: Motion (more control over backdrop + dialog)
---
Hero Sections & Landing Pages
Scenario: Marketing sites with parallax, scroll effects, complex animations
AutoAnimate:
- Not suitable
Motion:
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -300])
return <motion.div style={{ y }}>Hero content</motion.div>Recommendation: Motion (only option)
---
Carousels & Sliders
Scenario: Image galleries, product showcases
AutoAnimate:
- Not suitable
Motion:
<motion.div
drag="x"
dragConstraints={{ left: -width, right: 0 }}
>
{images.map(img => <img src={img.url} />)}
</motion.div>Recommendation: Motion (gesture controls required)
---
Drag-and-Drop Interfaces
Scenario: Kanban boards, sortable lists, reorderable items
AutoAnimate:
- Not supported (no drag gestures)
Motion:
<motion.div
drag
dragConstraints={constraints}
onDragEnd={handleDragEnd}
>
Draggable item
</motion.div>Recommendation: Motion (only option)
---
Page/Route Transitions
Scenario: Animating between pages or routes
AutoAnimate:
- Not ideal (designed for element-level, not page-level)
Motion:
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
>
{page content}
</motion.div>
</AnimatePresence>Recommendation: Motion (better control)
Note: Next.js App Router has issues with route transitions. Consider alternatives.
---
Card Expand to Detail View
Scenario: Grid of cards → click to expand → detail page
AutoAnimate:
- Not supported (no shared element transitions)
Motion:
// Grid view
<motion.div layoutId={card.id} onClick={expand}>
Card preview
</motion.div>
// Detail view
<motion.div layoutId={card.id}>
Full card details
</motion.div>Recommendation: Motion (shared element transition with layoutId)
---
Performance Comparison
| Metric | AutoAnimate | Motion |
|---|---|---|
| First paint | Fastest (3.28 KB) | Slow (34 KB) or Fast (2.3-4.6 KB with optimization) |
| Runtime performance | Fast (minimal JS) | Fast (GPU-accelerated when possible) |
| Large lists (50+ items) | Good (auto-optimized) | Poor (needs virtualization) |
| Scroll animations | N/A | Excellent (ScrollTimeline API) |
| Hardware acceleration | Yes | Yes |
Winner for first paint: AutoAnimate
Winner for runtime: Tie (both use GPU when possible)
Winner for large lists: AutoAnimate (no setup needed)
---
Accessibility Comparison
| Feature | AutoAnimate | Motion |
|---|---|---|
| prefers-reduced-motion | ✅ Automatic | ✅ Manual (MotionConfig) |
| Keyboard support | ✅ Inherits from elements | ✅ whileFocus prop |
| Screen reader friendly | ✅ Yes | ✅ Yes |
Winner: AutoAnimate (automatic reduced motion support)
Note: Motion requires manual setup:
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>---
Framework Compatibility
| Framework | AutoAnimate | Motion |
|---|---|---|
| React 18/19 | ✅ Full support | ✅ Full support |
| Next.js Pages Router | ✅ Works out of the box | ✅ Works out of the box |
| Next.js App Router | ✅ Works out of the box | ✅ Requires "use client" |
| Vite | ✅ Works out of the box | ✅ Works out of the box |
| Cloudflare Workers | ✅ Full support | ⚠️ Use framer-motion v12 |
| Remix | ✅ Works out of the box | ✅ Works out of the box |
| Astro | ✅ Via React component | ✅ Via React component |
| SvelteKit | ✅ Via Svelte plugin | ❌ React only |
| Vue/Nuxt | ✅ Via Vue plugin | ❌ React only |
Winner for compatibility: AutoAnimate (supports Vue, Svelte, vanilla JS)
Note: Motion is React-only, but AutoAnimate has official plugins for:
- Vue:
@formkit/auto-animate/vue - Svelte:
@formkit/auto-animate/svelte - Vanilla JS:
@formkit/auto-animate
---
Migration Guide
From AutoAnimate to Motion
When to migrate: You outgrow AutoAnimate and need gestures, scroll, or layout animations.
Example - Animated List:
Before (AutoAnimate):
const [parent] = useAutoAnimate()
return <ul ref={parent}>{items.map(...)}</ul>After (Motion):
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
layout
>
{item.text}
</motion.li>
))}
</AnimatePresence>Migration steps: 1. Install Motion: pnpm add motion 2. Replace <div ref={parent}> with <motion.div> 3. Add animation props: initial, animate, exit 4. Wrap in <AnimatePresence> for exit animations 5. Add layout prop for reordering
---
From Motion to AutoAnimate
When to migrate: Simplifying codebase, reducing bundle size for simple animations.
Example - Animated List:
Before (Motion):
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
layout
>
{item.text}
</motion.li>
))}
</AnimatePresence>After (AutoAnimate):
const [parent] = useAutoAnimate()
return <ul ref={parent}>{items.map(...)}</ul>Migration steps: 1. Install AutoAnimate: pnpm add @formkit/auto-animate 2. Remove Motion props (initial, animate, exit, layout) 3. Remove AnimatePresence wrapper 4. Add useAutoAnimate hook and attach ref to parent
Bundle size savings: 34 KB → 3.28 KB (~90% reduction)
---
Real-World Recommendations
E-commerce Site
Use AutoAnimate for:
- Shopping cart item add/remove
- Product filter results
- Notification toasts
Use Motion for:
- Product image carousel (drag gestures)
- Hero section parallax
- Product detail page transitions (shared elements)
Blog / Content Site
Use AutoAnimate for:
- Article list filtering
- Comment threads expand/collapse
- Tag selection
Use Motion for:
- Hero parallax on homepage
- Scroll-triggered section reveals
- Image lightbox modals
Dashboard / SaaS App
Use AutoAnimate for:
- Sidebar navigation accordion
- Data table row add/remove
- Toast notifications
Use Motion for:
- Drag-to-reorder kanban cards
- Chart animations
- Modal dialogs with complex transitions
Landing Page / Marketing Site
Use AutoAnimate for:
- FAQ accordion
- Feature comparison table filtering
Use Motion for:
- Hero section (parallax, scroll effects)
- Scroll-triggered reveals throughout page
- Interactive demos (gestures, drag)
---
Cost-Benefit Analysis
AutoAnimate
Benefits:
- ✅ Smallest bundle (3.28 KB)
- ✅ Zero configuration
- ✅ Works with any framework (React, Vue, Svelte, vanilla)
- ✅ Automatic prefers-reduced-motion
- ✅ Perfect for 90% of animation needs
Costs:
- ❌ No gesture controls
- ❌ No scroll animations
- ❌ No layout/shared element transitions
- ❌ No SVG path morphing
- ❌ Less control over animation timing/easing
Motion
Benefits:
- ✅ Complete animation toolkit
- ✅ Gesture controls (drag, hover, tap, pan)
- ✅ Scroll-based animations
- ✅ Layout/shared element transitions
- ✅ SVG path morphing and line drawing
- ✅ Spring physics customization
- ✅ Hardware-accelerated (ScrollTimeline API)
Costs:
- ❌ Larger bundle (34 KB, optimizable to 2.3-4.6 KB)
- ❌ More complex API
- ❌ React-only
- ❌ Requires manual prefers-reduced-motion setup
- ❌ Cloudflare Workers compatibility issues
---
Decision Flowchart
Start
↓
Do you need gestures (drag, hover with fine control)?
├─ Yes → Motion
└─ No → ↓
Do you need scroll-based animations or parallax?
├─ Yes → Motion
└─ No → ↓
Do you need shared element transitions (card → detail)?
├─ Yes → Motion
└─ No → ↓
Do you need SVG path morphing or line drawing?
├─ Yes → Motion
└─ No → ↓
Is it just list add/remove/sort animations?
├─ Yes → AutoAnimate
└─ No → ↓
Is it accordion/collapse/expand?
├─ Yes → AutoAnimate (unless you need custom physics)
└─ No → ↓
Do you want zero configuration?
├─ Yes → AutoAnimate
└─ No → Motion---
Can You Use Both?
Yes! They complement each other well.
Pattern:
- Use AutoAnimate for simple list animations
- Use Motion for complex gestures and scroll effects
Example:
// Simple list with AutoAnimate
const [listRef] = useAutoAnimate()
return (
<>
{/* Motion for hero parallax */}
<motion.div style={{ y: parallaxY }}>
Hero section
</motion.div>
{/* AutoAnimate for product list */}
<ul ref={listRef}>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
{/* Motion for carousel */}
<motion.div drag="x">
{images.map(img => <img src={img.url} />)}
</motion.div>
</>
)Bundle size: 3.28 KB (AutoAnimate) + 2.3-34 KB (Motion) = 5.58-37.28 KB total
Recommendation: Use AutoAnimate for 90% of cases, add Motion only for features AutoAnimate can't handle.
---
Summary Table
| Criteria | AutoAnimate | Motion | Winner |
|---|---|---|---|
| Bundle Size | 3.28 KB | 2.3-34 KB | AutoAnimate |
| API Simplicity | 3 lines of code | 12+ lines | AutoAnimate |
| Feature Set | Limited | Comprehensive | Motion |
| Gesture Controls | ❌ | ✅ | Motion |
| Scroll Animations | ❌ | ✅ | Motion |
| Layout Animations | ❌ | ✅ | Motion |
| SVG Animations | ❌ | ✅ | Motion |
| List Animations | ✅ Automatic | ✅ Manual | AutoAnimate |
| Accessibility | ✅ Automatic | ✅ Manual | AutoAnimate |
| Framework Support | React, Vue, Svelte, JS | React only | AutoAnimate |
| Cloudflare Workers | ✅ | ⚠️ | AutoAnimate |
| Learning Curve | Minutes | Hours | AutoAnimate |
| Performance | Excellent | Excellent | Tie |
---
Final Recommendation
Default to AutoAnimate for:
- List animations (add/remove/sort)
- Simple accordions
- Toast notifications
- Form validation errors
Upgrade to Motion when you need:
- Gestures (drag, hover with fine control)
- Scroll animations or parallax
- Shared element transitions
- SVG path morphing
- Complex choreographed animations
Use both when building complex apps with diverse animation needs.
80/20 Rule: 80% of your animations can be handled by AutoAnimate, 20% require Motion.
---
Getting Help
- AutoAnimate: https://auto-animate.formkit.com
- Motion: https://motion.dev
- AutoAnimate Skill: See
../SKILL.mdin this repo - Motion Skill: See
../SKILL.mdin this repo
Motion + Next.js Integration Guide
This guide covers how to use Motion (Framer Motion) with Next.js, including App Router patterns, Pages Router setup, known issues, and performance optimization.
---
TL;DR - Quick Start
Pages Router (Next.js 12):
// Works out of the box, no special setup needed
import { motion } from "motion/react"
export default function Page() {
return <motion.div animate={{ opacity: 1 }}>Content</motion.div>
}App Router (Next.js 13+):
// MUST add "use client" directive
"use client"
import { motion } from "motion/react-client" // Optimized import
export default function Page() {
return <motion.div animate={{ opacity: 1 }}>Content</motion.div>
}---
App Router (Next.js 13, 14, 15)
Key Requirement: Client Components Only
Motion uses browser APIs (DOM, window, events) that don't exist on the server. Therefore, Motion only works in Client Components, not Server Components.
---
Pattern 1: Direct Client Component (Simplest)
Add "use client" directive at the top of any file using Motion:
File: src/app/page.tsx
"use client"
import { motion } from "motion/react-client" // Optimized for Next.js
export default function Page() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
Welcome to Next.js + Motion
</motion.div>
)
}Pros:
- ✅ Simple, straightforward
- ✅ Works immediately
Cons:
- ❌ Entire page becomes client-rendered
- ❌ Loses Server Component benefits (streaming, server-side data fetching)
---
Pattern 2: Wrapper Component (Recommended)
Create a reusable Client Component wrapper to avoid repeating "use client":
File: src/components/motion-client.tsx
"use client"
// Optimized import for Next.js (reduces client JS)
import * as motion from "motion/react-client"
export { motion }
// Also export commonly used components/hooks
export {
AnimatePresence,
MotionConfig,
LazyMotion,
LayoutGroup,
useMotionValue,
useTransform,
useScroll,
useSpring,
useAnimate,
useInView,
} from "motion/react-client"File: src/app/page.tsx (Server Component)
import { motion } from "@/components/motion-client"
export default function Page() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
This page is a Server Component!
Motion wrapper is a Client Component.
</motion.div>
)
}Pros:
- ✅ Server Components can use Motion via wrapper
- ✅ Only Motion components are client-rendered
- ✅ Cleaner imports (no need to repeat
"use client")
Cons:
- ❌ Slight indirection (one extra file)
---
Pattern 3: Server Data + Client Animation
Fetch data in Server Component, animate in Client Component:
File: src/components/AnimatedCard.tsx (Client Component)
"use client"
import { motion } from "motion/react-client"
interface Product {
id: number
name: string
price: number
}
export function AnimatedCard({ product, index }: { product: Product; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} // Stagger
whileHover={{ scale: 1.05 }}
className="p-4 bg-white border rounded-lg"
>
<h3 className="font-bold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</motion.div>
)
}File: src/app/products/page.tsx (Server Component)
import { AnimatedCard } from "@/components/AnimatedCard"
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
cache: 'force-cache' // Server-side caching
})
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts() // Server-side fetch
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product, index) => (
<AnimatedCard key={product.id} product={product} index={index} />
))}
</div>
)
}Pros:
- ✅ Data fetched on server (SEO, performance, security)
- ✅ Animations run on client (interactivity)
- ✅ Best of both worlds
Cons:
- ❌ Requires splitting into two components
---
Pattern 4: MotionConfig Provider
Wrap app in MotionConfig for global settings (reduced motion, transitions):
File: src/components/MotionProvider.tsx (Client Component)
"use client"
import { MotionConfig } from "motion/react-client"
import { ReactNode } from "react"
export function MotionProvider({ children }: { children: ReactNode }) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
)
}File: src/app/layout.tsx (Root Layout)
import { MotionProvider } from "@/components/MotionProvider"
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<MotionProvider>
{children}
</MotionProvider>
</body>
</html>
)
}Respects: macOS/Windows/iOS/Android "Reduce Motion" accessibility setting
---
Pages Router (Next.js 12)
No Special Setup Required
Motion works out of the box with Pages Router:
File: pages/index.tsx
import { motion } from "motion/react"
export default function HomePage() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Welcome
</motion.div>
)
}No `"use client"` needed - Pages Router renders on client by default.
---
Server-Side Rendering (SSR)
Motion is client-only, so you may see hydration warnings in development. This is expected and can be ignored.
If you see hydration errors:
Option 1: Dynamic Import
import dynamic from 'next/dynamic'
const AnimatedComponent = dynamic(
() => import('@/components/AnimatedComponent'),
{ ssr: false }
)
export default function Page() {
return <AnimatedComponent />
}Option 2: Conditional Rendering
import { useState, useEffect } from 'react'
import { motion } from 'motion/react'
export default function Page() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <div>Loading...</div>
}
return <motion.div animate={{ opacity: 1 }}>Content</motion.div>
}---
Known Issues
Issue 1: Next.js 15 + React 19 Compatibility
Status: Most issues resolved in latest Motion version (12.23.24)
Symptoms:
- Build errors: "unsupported to use 'export *' in a client boundary"
- Runtime errors with Server Components
Solution: Update to latest versions:
pnpm add motion@latest react@latest next@latestIf issues persist: Check GitHub for updates: https://github.com/motiondivision/motion/issues
---
Issue 2: AnimatePresence with Soft Navigation
Problem: Exit animations don't work when navigating between pages in App Router.
Why: Next.js soft navigation doesn't trigger React unmount, so AnimatePresence doesn't detect exit.
Solutions:
Option 1: Component-level AnimatePresence (Recommended)
// Use AnimatePresence for modals, dropdowns, tooltips
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>Option 2: template.tsx (Experimental)
// src/app/template.tsx
"use client"
import { motion } from "motion/react-client"
export default function Template({ children }: { children: ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)
}Note: template.tsx creates new instances on navigation, enabling enter animations but not exit animations.
Option 3: Middleware approach Use Next.js middleware to detect route changes and trigger animations manually. Complex, not recommended.
Recommendation: Accept that page-level exit animations don't work reliably in App Router. Use AnimatePresence for component-level animations (modals, dropdowns, etc.) where it works perfectly.
---
Issue 3: Reorder Component Incompatibility
Problem: Motion's <Reorder> component doesn't work with Next.js routing.
Symptoms:
- Random stuck states
- Items don't reorder
- Console errors
GitHub Issues: #2183, #2101
Solution: Use alternative drag-to-reorder implementations:
@dnd-kit/core(recommended)react-beautiful-dnd- Manual implementation with
dragprop
Example with `drag` prop:
<motion.div
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
whileDrag={{ scale: 1.05 }}
>
Draggable item
</motion.div>---
Issue 4: Large Bundle Size
Problem: Full Motion component adds ~34 KB to client bundle.
Solution: Use optimized import and LazyMotion:
File: src/components/motion-client.tsx
"use client"
import { LazyMotion, domAnimation } from "motion/react-client"
import { ReactNode } from "react"
export function MotionProvider({ children }: { children: ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
)
}
// Export 'm' component instead of 'motion'
export { m as motion } from "motion/react-client"Reduces bundle from 34 KB → 4.6 KB
See performance-optimization.md for full guide.
---
Issue 5: Reduced Motion Not Affecting AnimatePresence
Problem: MotionConfig reducedMotion prop doesn't disable AnimatePresence animations.
GitHub Issue: #1567
Workaround: Manual check:
"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "motion/react-client"
export function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)")
setPrefersReducedMotion(mediaQuery.matches)
const handleChange = () => setPrefersReducedMotion(mediaQuery.matches)
mediaQuery.addEventListener("change", handleChange)
return () => mediaQuery.removeEventListener("change", handleChange)
}, [])
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: prefersReducedMotion ? 1 : 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: prefersReducedMotion ? 1 : 0 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>
)
}---
Performance Optimization for Next.js
1. Use motion/react-client Import
Regular import (full bundle):
import { motion } from "motion/react"Optimized import (smaller bundle):
import { motion } from "motion/react-client"Difference: react-client variant excludes server-side code, reducing client JavaScript.
---
2. Code Splitting with Dynamic Imports
For animations not needed on initial load:
import dynamic from 'next/dynamic'
const AnimatedHero = dynamic(
() => import('@/components/AnimatedHero'),
{ ssr: false }
)
export default function HomePage() {
return <AnimatedHero />
}Benefits:
- ✅ Reduces initial JavaScript bundle
- ✅ Loads animation code only when needed
---
3. LazyMotion for Smaller Bundle
Setup:
"use client"
import { LazyMotion, domAnimation, m } from "motion/react-client"
export default function Layout({ children }: { children: ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
)
}Then use `m` instead of `motion`:
"use client"
import { m } from "motion/react-client"
export function Component() {
return <m.div animate={{ opacity: 1 }}>Content</m.div>
}Bundle size: 34 KB → 4.6 KB
---
4. Optimize Images with Next/Image
Combine Motion with Next.js Image optimization:
"use client"
import { motion } from "motion/react-client"
import Image from "next/image"
export function AnimatedImage() {
return (
<motion.div whileHover={{ scale: 1.05 }}>
<Image
src="/hero.jpg"
width={1200}
height={600}
alt="Hero"
priority
/>
</motion.div>
)
}Benefits:
- ✅ Automatic image optimization
- ✅ Smooth animations
---
Testing & Debugging
1. Verify Client Component Boundary
Problem: Accidentally using Motion in Server Component.
Check: 1. Look for "use client" directive at top of file 2. If using wrapper, verify wrapper has "use client"
Error message:
Error: motion is not definedFix: Add "use client" to the file.
---
2. Check Bundle Size
Analyze:
pnpm build
# Then check .next/analyze or use @next/bundle-analyzerTarget: Motion should be <5 KB (with LazyMotion)
If larger: Switch to LazyMotion or useAnimate mini.
---
3. Test Reduced Motion
Enable in OS:
- macOS: System Settings → Accessibility → Display → Reduce motion
- Windows: Settings → Ease of Access → Display → Show animations
- iOS: Settings → Accessibility → Motion
- Android 9+: Settings → Accessibility → Remove animations
Verify: Animations should be instant (no transitions).
---
4. Lighthouse Performance
Run:
pnpm build
pnpm start
# Open Chrome DevTools → Lighthouse → Run analysisTarget Scores:
- Performance: >90
- Accessibility: 100
If low performance: Check for:
- Large bundle size (optimize with LazyMotion)
- Too many animated elements (use virtualization)
- Non-accelerated animations (use transform, not width/height)
---
Deployment Checklist
Before deploying Next.js + Motion:
- [ ] All Motion files have
"use client"directive (App Router) - [ ] Using
motion/react-clientimport (notmotion/react) - [ ] LazyMotion enabled (if bundle size matters)
- [ ] MotionConfig with reducedMotion set up
- [ ] No Motion usage in Server Components
- [ ] AnimatePresence only for component-level animations (not routes)
- [ ] Images optimized with next/image
- [ ] Tested with prefers-reduced-motion enabled
- [ ] Bundle analyzed (<5 KB for Motion recommended)
- [ ] Lighthouse performance score >90
---
Quick Reference
App Router
| Task | Solution |
|---|---|
| Use Motion | Add "use client" to file |
| Optimize import | import from "motion/react-client" |
| Reduce bundle | Use LazyMotion |
| Global config | Wrap in MotionProvider |
| Server data + animation | Fetch in Server Component, animate in Client Component |
| Route transitions | Not reliable, use component-level only |
Pages Router
| Task | Solution |
|---|---|
| Use Motion | Just import and use (no setup) |
| Avoid hydration errors | Use dynamic import with ssr: false |
| Optimize bundle | Use LazyMotion |
---
Getting Help
- Next.js Docs: https://nextjs.org/docs
- Motion Docs: https://motion.dev/docs/react
- Motion + Next.js Issues: https://github.com/motiondivision/motion/issues
- Stack Overflow: Tag:
framer-motion+next.js
---
Key Takeaway: For App Router, always use "use client" and motion/react-client import. For Pages Router, it just works. Use LazyMotion to keep bundle size small.
Motion Performance Optimization
This guide covers techniques to optimize Motion animations for production, including bundle size reduction, runtime performance improvements, and best practices.
---
Bundle Size Optimization
Problem: Full Motion Component is 34 KB
The full motion component includes all animation features, resulting in ~34 KB minified+gzipped. For many use cases, this is overkill.
---
Solution 1: LazyMotion (Recommended)
Reduces bundle from 34 KB → 4.6 KB
LazyMotion loads animation features on-demand instead of bundling everything upfront.
Setup:
import { LazyMotion, domAnimation, m } from "motion/react"
function App() {
return (
<LazyMotion features={domAnimation}>
{/* Use 'm' instead of 'motion' */}
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
whileHover={{ scale: 1.1 }}
>
Content
</m.div>
</LazyMotion>
)
}Key changes: 1. Wrap app in <LazyMotion features={domAnimation}> 2. Use m component instead of motion 3. Import m from motion/react (not motion)
Features included with `domAnimation`:
- ✅ Transform animations (x, y, scale, rotate, etc.)
- ✅ Opacity animations
- ✅ Gestures (hover, tap, drag, pan)
- ✅ Layout animations
- ✅ useScroll, useTransform hooks
- ❌ SVG path animations (use
domMaxinstead) - ❌ Custom value types
When to use `domMax` instead of `domAnimation`:
import { LazyMotion, domMax, m } from "motion/react"
<LazyMotion features={domMax}>
{/* Now includes SVG path animations */}
<m.path d="..." animate={{ pathLength: 1 }} />
</LazyMotion>Bundle size: domAnimation (4.6 KB), domMax (~6 KB)
---
Solution 2: useAnimate Mini (Smallest)
Reduces bundle from 34 KB → 2.3 KB
The useAnimate mini variant is the smallest React animation library available. Use for imperative animations.
Setup:
import { useAnimate } from "motion/react"
import { useEffect } from "react"
function Component() {
const [scope, animate] = useAnimate()
useEffect(() => {
animate(scope.current, { opacity: 1, x: 0 })
}, [])
return (
<div ref={scope} style={{ opacity: 0, transform: "translateX(-20px)" }}>
Content
</div>
)
}Pros:
- ✅ Smallest bundle size (2.3 KB)
- ✅ Imperative API (full control)
- ✅ No component overhead
Cons:
- ❌ More verbose than declarative API
- ❌ Less ergonomic for complex animations
When to use:
- Bundle size is absolutely critical
- You prefer imperative animations
- Simple animations (fade in, slide in, etc.)
---
Solution 3: useAnimate Hybrid
Reduces bundle from 34 KB → 17 KB
The hybrid version includes more features than mini but less than full.
Setup:
import { useAnimate, stagger } from "motion/react"
function Component() {
const [scope, animate] = useAnimate()
const handleAnimate = () => {
animate("li", { opacity: 1, x: 0 }, { delay: stagger(0.1) })
}
return (
<ul ref={scope}>
{items.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
)
}Bundle size: 17 KB
---
Solution 4: Remove Unused Features
If you only use Motion for specific features, import only what you need:
Example - Only scroll animations:
import { useScroll, useTransform } from "motion/react"
import { useRef } from "react"
function Component() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({ container: ref })
const y = useTransform(scrollYProgress, [0, 1], [0, -100])
return (
<div ref={ref} style={{ transform: `translateY(${y}px)` }}>
Content
</div>
)
}No `motion` component imported → smaller bundle
---
Bundle Size Comparison
| Approach | Bundle Size | Features | Best For |
|---|---|---|---|
| Full motion | 34 KB | All features | Kitchen sink approach |
| LazyMotion + domAnimation | 4.6 KB | Most features | Recommended default |
| LazyMotion + domMax | ~6 KB | All features except custom | SVG animations |
| useAnimate hybrid | 17 KB | Imperative + stagger | Imperative animations |
| useAnimate mini | 2.3 KB | Basic imperative | Smallest bundle |
| Hooks only | <5 KB | Specific features | Scroll/transform only |
Recommendation: Start with LazyMotion + domAnimation (4.6 KB) for 90% of use cases.
---
Runtime Performance Optimization
1. Add willChange for Transforms
Problem: Browser doesn't know which properties will animate, causing reflows.
Solution: Tell browser to optimize for animation.
<motion.div
style={{ willChange: "transform" }}
animate={{ x: 100, rotate: 45 }}
/>Also add for:
opacitybackgroundColorclipPathfilter
How it works:
- Browser promotes element to its own layer
- Uses GPU compositing
- Avoids reflow/repaint during animation
Warning: Don't overuse! Only add to elements that actually animate.
---
2. Use Hardware-Accelerated Properties
Good properties (GPU-accelerated):
- ✅
transform(x, y, scale, rotate, skew) - ✅
opacity - ✅
filter(blur, brightness, etc.)
Bad properties (causes reflow):
- ❌
width,height - ❌
top,left,right,bottom - ❌
padding,margin
Example - Wrong:
<motion.div
animate={{ width: 300, height: 200 }} // ❌ Causes layout reflow
/>Example - Correct:
<motion.div
animate={{ scale: 1.5 }} // ✅ GPU-accelerated transform
/>Rule: Prefer transform over layout properties.
---
3. Use layout Prop for FLIP Animations
Problem: Animating width/height directly causes reflow.
Solution: Use layout prop for FLIP technique (First, Last, Invert, Play).
<motion.div layout>
{isExpanded ? <LargeContent /> : <SmallContent />}
</motion.div>How it works: 1. Measures element before change (First) 2. Applies change immediately (Last) 3. Inverts with transform to match first position 4. Animates to last position (Play)
Result: Smooth animation without reflow, all via GPU-accelerated transforms.
---
4. Optimize Scroll Animations
Motion uses native ScrollTimeline API when available for hardware-accelerated scroll animations.
Setup:
import { useScroll, useTransform } from "motion/react"
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -300])
<motion.div style={{ y }}>
Content
</motion.div>Performance:
- ✅ Runs on compositor thread (not main thread)
- ✅ 120fps on capable devices
- ✅ No JavaScript execution on scroll
Fallback: On browsers without ScrollTimeline API, Motion falls back to JavaScript requestAnimationFrame.
---
5. Debounce Complex Calculations
For expensive calculations in scroll/transform hooks, debounce updates:
Problem:
const { scrollY } = useScroll()
// Recalculates on every pixel scrolled
const complexValue = useTransform(scrollY, (value) => {
return expensiveCalculation(value) // ❌ Too frequent
})Solution:
const { scrollY } = useScroll()
// Only update when scroll changes by 10px
const complexValue = useTransform(scrollY, (value) => {
return Math.floor(value / 10) * 10
})---
6. Use transition Type Wisely
Different transition types have different performance characteristics:
Spring (default):
transition={{ type: "spring", stiffness: 100, damping: 10 }}- ✅ Natural, physics-based motion
- ❌ More JavaScript calculation
Tween:
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}- ✅ Predictable timing
- ✅ Less JavaScript calculation
- ❌ Less natural than spring
Recommendation: Use tween for simple animations, spring for interactive gestures.
---
Large Lists Optimization
Problem: 50-100+ Animated Items Cause Severe Slowdown
Animating many items simultaneously overwhelms the browser.
---
Solution 1: Virtualization (Recommended)
Only render visible items.
Install:
pnpm add react-window
# or
pnpm add react-virtuoso
# or
pnpm add @tanstack/react-virtualExample with react-window:
import { FixedSizeList } from 'react-window'
import { motion } from 'motion/react'
function VirtualizedList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<motion.div
style={style}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{items[index].text}
</motion.div>
)}
</FixedSizeList>
)
}Benefits:
- ✅ Only renders ~20 items at a time (instead of 1000+)
- ✅ Dramatically reduces DOM nodes
- ✅ Maintains smooth 60fps
Drawback: Slightly more complex setup
---
Solution 2: Stagger with delayChildren
For moderately-sized lists (10-30 items), use stagger to spread out animations:
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.05, // 50ms delay between each child
delayChildren: 0.1, // 100ms delay before first child
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map(item => (
<motion.li key={item.id} variants={item}>
{item.text}
</motion.li>
))}
</motion.ul>Benefits:
- ✅ Avoids animating all items simultaneously
- ✅ Creates pleasing wave effect
- ✅ Reduces performance spike
Limit: Still struggles above ~50 items
---
Solution 3: Lazy Load with whileInView
Only animate items when they enter viewport:
{items.map(item => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
>
{item.content}
</motion.div>
))}Benefits:
- ✅ Only animates visible items
- ✅ Spreads performance cost over time
- ✅ No virtualization library needed
Drawback: Doesn't help with initial render if all items visible
---
Solution 4: Simplify Animations
For very large lists, simplify or remove animations:
const useReducedAnimations = items.length > 50
<motion.div
initial={{ opacity: useReducedAnimations ? 1 : 0 }}
animate={{ opacity: 1 }}
transition={{ duration: useReducedAnimations ? 0 : 0.3 }}
>
{item.content}
</motion.div>---
Performance Comparison (1000 Items)
| Approach | FPS | DOM Nodes | User Experience |
|---|---|---|---|
| No optimization | 5-10 fps | 1000+ | Unusable, browser hangs |
| Stagger only | 15-20 fps | 1000+ | Laggy, still poor |
| Virtualization | 60 fps | ~20 | Smooth, production-ready |
| whileInView | 40-50 fps | 1000+ | Acceptable for long lists |
| Simplified animations | 50-60 fps | 1000+ | Smooth but less polished |
Recommendation: Use virtualization for lists with 50+ items.
---
AnimatePresence Optimization
1. Use mode Prop
Wait mode (sequential):
<AnimatePresence mode="wait">
{isVisible && <motion.div key="content">Content</motion.div>}
</AnimatePresence>- Waits for exit animation to complete before entering
- Prevents both elements from being in DOM simultaneously
- Better performance
Sync mode (simultaneous):
<AnimatePresence mode="sync">
{isVisible && <motion.div key="content">Content</motion.div>}
</AnimatePresence>- Enter and exit happen simultaneously
- More DOM nodes temporarily
- Higher performance cost
Recommendation: Use mode="wait" for modals/dialogs, mode="sync" for crossfades.
---
2. Limit AnimatePresence Usage
Problem: Wrapping entire app in AnimatePresence adds overhead.
Solution: Only wrap components that actually exit:
Bad:
<AnimatePresence>
<Layout>
<StaticHeader />
<DynamicContent />
<StaticFooter />
</Layout>
</AnimatePresence>Good:
<Layout>
<StaticHeader />
<AnimatePresence>
<DynamicContent />
</AnimatePresence>
<StaticFooter />
</Layout>---
Layout Animation Performance
1. Use layoutId for Shared Elements
Problem: Multiple separate layout animations calculated independently.
Solution: Use layoutId to connect related elements:
// Card view
<motion.div layoutId={card.id}>
<CardPreview />
</motion.div>
// Detail view
<motion.div layoutId={card.id}>
<CardDetail />
</motion.div>Performance: Motion knows these are the same element, optimizes FLIP calculation.
---
2. Add layoutRoot for Fixed Elements
Problem: Fixed-position elements cause expensive layout calculations.
Solution: Mark as layout root to isolate calculations:
<motion.div
layoutRoot
layout
className="fixed top-0 left-0"
>
Fixed content
</motion.div>---
3. Add layoutScroll for Scrollable Containers
Problem: Layout animations in scrolled containers are incomplete/broken.
Solution: Add layoutScroll to account for scroll offset:
<motion.div
layoutScroll
className="overflow-auto h-96"
>
{items.map(item => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
))}
</motion.div>---
Gesture Performance
1. Use dragMomentum={false} When Not Needed
Problem: Momentum calculations add overhead.
Solution: Disable if you don't need inertia:
<motion.div
drag
dragMomentum={false} // Disable inertia
>
Drag me
</motion.div>Use momentum for: Carousels, swiping Disable momentum for: Precise positioning, drag-to-reorder
---
2. Set dragElastic Lower
Problem: Higher elasticity = more calculations.
Solution: Use 0-0.2 for most cases:
<motion.div
drag
dragElastic={0.1} // Low elasticity
>
Drag me
</motion.div>Default: 0.5 (high) Recommended: 0.1-0.2 (medium) Performance mode: 0 (none)
---
Measuring Performance
1. React DevTools Profiler
Enable: 1. Install React DevTools 2. Open "Profiler" tab 3. Click record 4. Trigger animations 5. Stop recording
Look for:
- Flame graph spikes during animations
- Components re-rendering unnecessarily
- Long commit times
---
2. Browser Performance Tab
Enable: 1. Open Chrome DevTools 2. "Performance" tab 3. Click record 4. Trigger animations 5. Stop recording
Look for:
- Frame rate (should be 60fps or 120fps)
- Long JavaScript tasks (should be <16ms)
- Layout reflows (should be minimal)
- Paint operations (should be green, not red)
---
3. Frame Rate Monitor
Add visual FPS counter during development:
import { useState, useEffect } from "react"
function FPSMonitor() {
const [fps, setFps] = useState(0)
useEffect(() => {
let lastTime = performance.now()
let frames = 0
function loop() {
frames++
const now = performance.now()
if (now >= lastTime + 1000) {
setFps(Math.round((frames * 1000) / (now - lastTime)))
lastTime = now
frames = 0
}
requestAnimationFrame(loop)
}
loop()
}, [])
return (
<div className="fixed top-4 right-4 bg-black text-white p-2 rounded text-sm">
{fps} FPS
</div>
)
}Target: 60 FPS (or 120 FPS on high-refresh displays)
---
Production Checklist
Before deploying Motion animations, verify:
- [ ] Bundle size optimized (LazyMotion or useAnimate)
- [ ]
willChangeadded for animated transforms - [ ] Only GPU-accelerated properties used (transform, opacity)
- [ ]
layoutprop used instead of animating width/height directly - [ ] Large lists use virtualization (50+ items)
- [ ] Scroll animations use
whileInVieworuseScroll - [ ] AnimatePresence only wraps necessary components
- [ ]
mode="wait"used for modals (if applicable) - [ ]
layoutScrolladded to scrollable containers - [ ]
layoutRootadded to fixed elements - [ ] Tested on low-end devices (throttle CPU in DevTools)
- [ ] Tested with
prefers-reduced-motionenabled - [ ] Frame rate verified (60fps minimum)
- [ ] No console warnings from Motion
---
Performance Budget
Recommended limits for production:
| Metric | Target | Maximum |
|---|---|---|
| Bundle size (Motion) | <5 KB | 10 KB |
| Total JavaScript | <200 KB | 300 KB |
| Frame rate | 60 FPS | 40 FPS minimum |
| Animated elements (simultaneous) | <20 | <50 |
| AnimatePresence wrappers | <5 | <10 |
| Layout animations (simultaneous) | <10 | <20 |
---
Common Performance Anti-Patterns
❌ Anti-Pattern 1: Animating All List Items on Mount
{items.map(item => (
<motion.div
key={item.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }} // ❌ All 100 items animate at once
>
{item.content}
</motion.div>
))}Fix: Use stagger or whileInView:
const container = {
hidden: {},
show: {
transition: { staggerChildren: 0.05 }
}
}
<motion.div variants={container} initial="hidden" animate="show">
{items.map(item => (
<motion.div key={item.id} variants={item}>
{item.content}
</motion.div>
))}
</motion.div>---
❌ Anti-Pattern 2: No key Props with AnimatePresence
<AnimatePresence>
{items.map(item => (
<motion.div> {/* ❌ Missing key */}
{item.content}
</motion.div>
))}
</AnimatePresence>Fix: Always add unique key props:
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id}> {/* ✅ Unique key */}
{item.content}
</motion.div>
))}
</AnimatePresence>---
❌ Anti-Pattern 3: Animating Non-Accelerated Properties
<motion.div
animate={{ width: 300, top: 100 }} // ❌ Causes reflow
/>Fix: Use transforms:
<motion.div
animate={{ scale: 1.5, y: 100 }} // ✅ GPU-accelerated
/>---
❌ Anti-Pattern 4: Full Motion Bundle for Simple Use Case
import { motion } from "motion/react" // ❌ 34 KB for simple fade
<motion.div animate={{ opacity: 1 }}>Content</motion.div>Fix: Use LazyMotion or useAnimate:
import { LazyMotion, domAnimation, m } from "motion/react"
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }}>Content</m.div>
</LazyMotion>---
Getting Help
- Performance Issues: https://github.com/motiondivision/motion/issues
- Bundle Size Analysis: https://bundlephobia.com/package/motion
- Official Optimization Guide: https://motion.dev/docs/react-reduce-bundle-size
---
Key Takeaway: For 90% of use cases, use LazyMotion + domAnimation (4.6 KB) and follow the optimization checklist above. This provides excellent performance while maintaining a small bundle size.
#!/bin/bash
# Motion Setup Script
# Automates installation and initial setup for React + Vite + Next.js projects
set -e
echo "🎬 Motion Setup"
echo "==============="
echo ""
# Check if package.json exists
if [ ! -f "package.json" ]; then
echo "❌ Error: package.json not found"
echo " Run this script from your project root"
exit 1
fi
# Detect framework
FRAMEWORK="unknown"
USING_NEXTJS=false
USING_VITE=false
USING_CLOUDFLARE=false
if grep -q '"next"' package.json; then
FRAMEWORK="Next.js"
USING_NEXTJS=true
echo "✅ Detected: Next.js project"
elif grep -q '"vite"' package.json; then
FRAMEWORK="Vite"
USING_VITE=true
echo "✅ Detected: Vite project"
else
echo "⚠️ Could not detect framework (Next.js or Vite)"
echo " Will proceed with generic setup"
fi
# Check for Cloudflare Workers
if grep -q '@cloudflare/vite-plugin\|wrangler' package.json; then
USING_CLOUDFLARE=true
echo "🔍 Detected: Cloudflare Workers project"
echo ""
echo "⚠️ WARNING: Motion has build compatibility issues with Wrangler"
echo " Recommendation: Use framer-motion v12.23.24 instead"
echo ""
read -p "Continue with Motion (may cause build errors) or use framer-motion? [motion/framer]: " choice
if [ "$choice" = "framer" ]; then
PACKAGE="framer-motion"
else
PACKAGE="motion"
fi
else
PACKAGE="motion"
fi
echo ""
# Detect package manager
if command -v pnpm &> /dev/null; then
PKG_MANAGER="pnpm"
elif command -v yarn &> /dev/null; then
PKG_MANAGER="yarn"
else
PKG_MANAGER="npm"
fi
# Install Motion
echo "📦 Installing $PACKAGE using $PKG_MANAGER..."
if [ "$PKG_MANAGER" = "pnpm" ]; then
pnpm add $PACKAGE
elif [ "$PKG_MANAGER" = "yarn" ]; then
yarn add $PACKAGE
else
npm install $PACKAGE
fi
echo "✅ Package installed"
echo ""
# Create directories
mkdir -p src/components
mkdir -p src/hooks
# Next.js specific setup
if [ "$USING_NEXTJS" = true ]; then
echo "📝 Creating Next.js App Router Client Component wrapper..."
# Create motion-client.tsx wrapper
cat > src/components/motion-client.tsx << 'EOF'
"use client"
// Optimized import for Next.js (reduces client JS bundle)
import * as motion from "motion/react-client"
export { motion }
// Also export commonly used components
export {
AnimatePresence,
MotionConfig,
LazyMotion,
LayoutGroup,
useMotionValue,
useTransform,
useScroll,
useSpring,
useAnimate,
useInView,
useDragControls,
} from "motion/react-client"
EOF
echo "✅ Created: src/components/motion-client.tsx"
echo ""
# Create MotionProvider
cat > src/components/MotionProvider.tsx << 'EOF'
"use client"
import { MotionConfig } from "motion/react-client"
import { ReactNode } from "react"
export function MotionProvider({ children }: { children: ReactNode }) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
)
}
EOF
echo "✅ Created: src/components/MotionProvider.tsx"
echo ""
# Create example component
cat > src/components/AnimatedButton.tsx << 'EOF'
"use client"
import { motion } from "motion/react-client"
export function AnimatedButton() {
return (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold"
>
Hover and Click Me
</motion.button>
)
}
EOF
echo "✅ Created: src/components/AnimatedButton.tsx"
echo ""
fi
# Vite specific setup
if [ "$USING_VITE" = true ]; then
echo "📝 Creating Vite example component..."
cat > src/components/AnimatedExample.tsx << 'EOF'
import { motion } from "motion/react"
export function AnimatedExample() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="p-6 bg-blue-100 rounded-lg"
>
<h2 className="text-xl font-bold">Motion Animation</h2>
<p>This component fades in and slides up on mount.</p>
</motion.div>
)
}
EOF
echo "✅ Created: src/components/AnimatedExample.tsx"
echo ""
fi
# Summary
echo "✨ Setup complete!"
echo ""
echo "Next steps:"
echo ""
if [ "$USING_NEXTJS" = true ]; then
echo "1. Add MotionProvider to your root layout:"
echo " // src/app/layout.tsx"
echo " import { MotionProvider } from '@/components/MotionProvider'"
echo " "
echo " export default function RootLayout({ children }) {"
echo " return ("
echo " <html>"
echo " <body>"
echo " <MotionProvider>"
echo " {children}"
echo " </MotionProvider>"
echo " </body>"
echo " </html>"
echo " )"
echo " }"
echo ""
echo "2. Use Motion in any component:"
echo " import { motion } from '@/components/motion-client'"
echo " "
echo " <motion.div animate={{ opacity: 1 }}>Content</motion.div>"
echo ""
echo "3. See example:"
echo " import { AnimatedButton } from '@/components/AnimatedButton'"
echo ""
elif [ "$USING_VITE" = true ]; then
echo "1. Import the example component:"
echo " import { AnimatedExample } from '@/components/AnimatedExample'"
echo ""
echo "2. Use it in your app:"
echo " <AnimatedExample />"
echo ""
fi
echo "4. Check templates/ folder for more examples:"
echo " - Modal dialogs"
echo " - Accordions"
echo " - Carousels"
echo " - Scroll animations"
echo " - Layout transitions"
echo ""
if [ "$USING_CLOUDFLARE" = true ]; then
echo "⚠️ IMPORTANT (Cloudflare Workers):"
echo " Monitor GitHub issue #2918 for Motion + Wrangler compatibility"
echo " Consider using framer-motion v12.23.24 if build errors occur"
echo ""
fi
echo "📚 Documentation:"
echo " - Official: https://motion.dev/docs/react"
echo " - Skill docs: ../SKILL.md"
echo ""
#!/bin/bash
# Motion Bundle Optimizer
# Converts full motion component to LazyMotion for smaller bundle (34 KB → 4.6 KB)
set -e
echo "📦 Motion Bundle Optimizer"
echo "=========================="
echo ""
# Check if we're in a project with Motion
if [ ! -f "package.json" ]; then
echo "❌ Error: package.json not found"
exit 1
fi
if ! grep -q '"motion"\|"framer-motion"' package.json; then
echo "❌ Error: Motion or Framer Motion not found in package.json"
exit 1
fi
echo "✅ Motion detected"
echo ""
# Estimate current bundle impact
echo "📊 Current bundle impact: ~34 KB (full motion component)"
echo "📊 After optimization: ~4.6 KB (LazyMotion + domAnimation)"
echo "📊 Savings: ~29.4 KB (~86% reduction)"
echo ""
echo "⚠️ This script will:"
echo " 1. Show you how to set up LazyMotion"
echo " 2. Provide conversion examples"
echo " 3. NOT automatically modify your code (manual conversion required)"
echo ""
read -p "Continue? [y/N]: " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
echo ""
echo "===================="
echo "STEP 1: Add LazyMotion Provider"
echo "===================="
echo ""
echo "Create a LazyMotion wrapper component:"
echo ""
cat << 'EOF'
// src/components/MotionProvider.tsx (or app/providers.tsx for Next.js)
"use client" // Add this for Next.js App Router
import { LazyMotion, domAnimation } from "motion/react"
import { ReactNode } from "react"
export function MotionProvider({ children }: { children: ReactNode }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
)
}
EOF
echo ""
echo "Then wrap your app:"
echo ""
cat << 'EOF'
// For Vite: src/main.tsx or src/App.tsx
import { MotionProvider } from "@/components/MotionProvider"
function App() {
return (
<MotionProvider>
<YourApp />
</MotionProvider>
)
}
// For Next.js: app/layout.tsx
import { MotionProvider } from "@/components/MotionProvider"
export default function RootLayout({ children }) {
return (
<html>
<body>
<MotionProvider>
{children}
</MotionProvider>
</body>
</html>
)
}
EOF
echo ""
echo "===================="
echo "STEP 2: Convert Components"
echo "===================="
echo ""
echo "Change all 'motion' imports to 'm':"
echo ""
cat << 'EOF'
// BEFORE:
import { motion } from "motion/react"
<motion.div animate={{ x: 100 }} />
// AFTER:
import { m } from "motion/react"
<m.div animate={{ x: 100 }} />
EOF
echo ""
echo "===================="
echo "STEP 3: Find and Replace"
echo "===================="
echo ""
echo "Use find-and-replace in your editor:"
echo ""
echo "1. Find: import { motion"
echo " Replace: import { m"
echo ""
echo "2. Find: from \"motion/react\""
echo " Keep as is (or use motion/react-client for Next.js)"
echo ""
echo "3. Find: <motion."
echo " Replace: <m."
echo ""
echo "4. Find: </motion."
echo " Replace: </m."
echo ""
echo "===================="
echo "STEP 4: Verify Bundle Size"
echo "===================="
echo ""
echo "After converting, build your project and check bundle size:"
echo ""
cat << 'EOF'
# Vite
pnpm build
# Check dist/ folder size
# Next.js
pnpm build
# Check .next/ folder size or use @next/bundle-analyzer
EOF
echo ""
echo "Expected results:"
echo " Before: ~34 KB for motion"
echo " After: ~4.6 KB for LazyMotion + domAnimation"
echo ""
echo "===================="
echo "STEP 5: Features Included"
echo "===================="
echo ""
echo "domAnimation includes:"
echo " ✅ Transform animations (x, y, scale, rotate)"
echo " ✅ Opacity animations"
echo " ✅ Gestures (hover, tap, drag, pan)"
echo " ✅ Layout animations"
echo " ✅ useScroll, useTransform hooks"
echo " ❌ SVG path animations (use domMax instead)"
echo ""
echo "If you need SVG animations, use domMax:"
echo ""
cat << 'EOF'
import { LazyMotion, domMax } from "motion/react"
<LazyMotion features={domMax}>
{/* Now includes SVG */}
</LazyMotion>
EOF
echo ""
echo "Bundle size with domMax: ~6 KB (still better than 34 KB)"
echo ""
echo "===================="
echo "TROUBLESHOOTING"
echo "===================="
echo ""
echo "Issue: Animations not working after conversion"
echo "Solution: Verify LazyMotion wrapper is at root of component tree"
echo ""
echo "Issue: SVG animations not working"
echo "Solution: Use domMax instead of domAnimation"
echo ""
echo "Issue: Still seeing large bundle"
echo "Solution: Clear cache and rebuild"
echo ""
echo "===================="
echo "OPTIMIZATION COMPLETE"
echo "===================="
echo ""
echo "Summary:"
echo " 1. ✅ Wrap app in <LazyMotion features={domAnimation}>"
echo " 2. ✅ Change all motion.* to m.*"
echo " 3. ✅ Rebuild and verify bundle size"
echo ""
echo "Expected bundle reduction: ~86% (34 KB → 4.6 KB)"
echo ""
echo "📚 Full guide: ../references/performance-optimization.md"
echo ""
// Motion + Next.js App Router - Client Component Pattern
// Production-tested with Motion v12.23.24, Next.js 15, React 19
/**
* INSTALLATION
*
* 1. Install Motion:
* pnpm add motion
*
* 2. Create this file structure:
* src/components/motion-client.tsx ← This file (wrapper)
* src/components/AnimatedModal.tsx ← Example usage
* src/app/page.tsx ← Server Component can import
*
* 3. CRITICAL: Motion only works in Client Components, NOT Server Components
*
* NO NEXT.JS CONFIGURATION NEEDED - just use "use client" directive
*/
// ============================================================================
// PATTERN 1: Client Component Wrapper (Recommended for Next.js App Router)
// ============================================================================
/**
* File: src/components/motion-client.tsx
*
* Create a wrapper that re-exports Motion as a Client Component.
* This allows you to import { motion } in any file without repeating "use client".
*/
"use client"
// Optimized import for Next.js (reduces client JS bundle)
import * as motion from "motion/react-client"
// Re-export everything from motion
export { motion }
// Also export commonly used components
export {
AnimatePresence,
MotionConfig,
LazyMotion,
LayoutGroup,
useMotionValue,
useTransform,
useScroll,
useSpring,
useAnimate,
useInView,
useDragControls,
} from "motion/react-client"
/**
* USAGE IN SERVER COMPONENTS
*
* // src/app/page.tsx (Server Component)
* import { motion } from "@/components/motion-client"
*
* export default function Page() {
* return (
* <motion.div
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* >
* This works! The wrapper is a Client Component.
* </motion.div>
* )
* }
*/
// ============================================================================
// PATTERN 2: Direct Client Component
// ============================================================================
/**
* File: src/components/AnimatedModal.tsx
*
* For complex components, create dedicated Client Components.
*/
"use client"
import { motion, AnimatePresence } from "motion/react-client"
import { useState, ReactNode } from "react"
interface AnimatedModalProps {
trigger: ReactNode
title: string
children: ReactNode
}
export function AnimatedModal({ trigger, title, children }: AnimatedModalProps) {
const [isOpen, setIsOpen] = useState(false)
return (
<>
{/* Trigger button */}
<div onClick={() => setIsOpen(true)}>
{trigger}
</div>
{/* Modal with AnimatePresence */}
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsOpen(false)}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Dialog */}
<motion.dialog
key="dialog"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: "spring", damping: 20, stiffness: 300 }}
className="fixed inset-0 m-auto w-full max-w-md h-fit bg-white rounded-lg shadow-xl z-50 p-6"
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">{title}</h2>
<button
onClick={() => setIsOpen(false)}
className="text-gray-500 hover:text-gray-700"
>
✕
</button>
</div>
<div>{children}</div>
</motion.dialog>
</>
)}
</AnimatePresence>
</>
)
}
/**
* USAGE IN SERVER COMPONENT
*
* // src/app/page.tsx (Server Component)
* import { AnimatedModal } from "@/components/AnimatedModal"
*
* export default function Page() {
* return (
* <AnimatedModal
* trigger={<button>Open Modal</button>}
* title="Hello World"
* >
* <p>Modal content here</p>
* </AnimatedModal>
* )
* }
*/
// ============================================================================
// PATTERN 3: Reduced Motion for Accessibility
// ============================================================================
/**
* File: src/components/MotionProvider.tsx
*
* Wrap your app to respect user's prefers-reduced-motion setting.
*/
"use client"
import { MotionConfig } from "motion/react-client"
import { ReactNode } from "react"
interface MotionProviderProps {
children: ReactNode
}
export function MotionProvider({ children }: MotionProviderProps) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
)
}
/**
* USAGE IN ROOT LAYOUT
*
* // src/app/layout.tsx (Server Component)
* import { MotionProvider } from "@/components/MotionProvider"
*
* export default function RootLayout({ children }) {
* return (
* <html>
* <body>
* <MotionProvider>
* {children}
* </MotionProvider>
* </body>
* </html>
* )
* }
*
* This respects OS-level accessibility settings:
* - macOS: System Settings → Accessibility → Display → Reduce motion
* - Windows: Settings → Ease of Access → Display → Show animations
* - iOS: Settings → Accessibility → Motion
* - Android 9+: Settings → Accessibility → Remove animations
*/
// ============================================================================
// PATTERN 4: Page Transitions with App Router
// ============================================================================
/**
* File: src/components/PageTransition.tsx
*
* Animate route changes in App Router.
*/
"use client"
import { motion } from "motion/react-client"
import { ReactNode } from "react"
interface PageTransitionProps {
children: ReactNode
}
export function PageTransition({ children }: PageTransitionProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)
}
/**
* USAGE IN PAGE
*
* // src/app/about/page.tsx (Server Component)
* import { PageTransition } from "@/components/PageTransition"
*
* export default function AboutPage() {
* return (
* <PageTransition>
* <h1>About Page</h1>
* <p>Content animates in on route change</p>
* </PageTransition>
* )
* }
*
* Note: Exit animations require AnimatePresence, which may not work
* reliably with Next.js soft navigation. For full page transitions,
* consider using template.tsx file or middleware approach.
*/
// ============================================================================
// PATTERN 5: Server Data with Client Animation
// ============================================================================
/**
* File: src/components/AnimatedProductCard.tsx
*
* Fetch data in Server Component, animate in Client Component.
*/
"use client"
import { motion } from "motion/react-client"
interface Product {
id: number
name: string
price: number
image: string
}
interface AnimatedProductCardProps {
product: Product
index: number
}
export function AnimatedProductCard({ product, index }: AnimatedProductCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} // Stagger
whileHover={{ scale: 1.05 }}
className="p-4 bg-white border rounded-lg shadow-sm cursor-pointer"
>
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded mb-4"
/>
<h3 className="text-lg font-bold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</motion.div>
)
}
/**
* USAGE WITH SERVER DATA
*
* // src/app/products/page.tsx (Server Component)
* import { AnimatedProductCard } from "@/components/AnimatedProductCard"
*
* async function getProducts() {
* const res = await fetch('https://api.example.com/products')
* return res.json()
* }
*
* export default async function ProductsPage() {
* const products = await getProducts() // Server-side fetch
*
* return (
* <div className="grid grid-cols-3 gap-4">
* {products.map((product, index) => (
* <AnimatedProductCard
* key={product.id}
* product={product}
* index={index}
* />
* ))}
* </div>
* )
* }
*
* Benefits:
* - Data fetched on server (SEO, performance)
* - Animation runs on client (interactivity)
* - Best of both worlds
*/
// ============================================================================
// KNOWN ISSUES & WORKAROUNDS
// ============================================================================
/**
* ISSUE 1: "motion is not defined" or SSR Errors
*
* Cause: Forgot "use client" directive
*
* Solution: Add "use client" at top of file:
*
* "use client"
* import { motion } from "motion/react"
*/
/**
* ISSUE 2: AnimatePresence Exit Animations Not Working
*
* Cause: Next.js soft navigation doesn't trigger React unmount
*
* Solution: Use route-level AnimatePresence is unreliable in App Router.
* For component-level modals/dropdowns, it works fine. For page transitions,
* consider alternatives like view transitions API or middleware approach.
*
* GitHub issue: Check Next.js docs for latest recommendations
*/
/**
* ISSUE 3: Large Bundle Size
*
* Cause: Full motion component is ~34 KB minified+gzipped
*
* Solution: Use LazyMotion for 4.6 KB:
*
* "use client"
*
* import { LazyMotion, domAnimation, m } from "motion/react-client"
*
* export function App() {
* return (
* <LazyMotion features={domAnimation}>
* <m.div animate={{ x: 100 }} />
* </LazyMotion>
* )
* }
*
* See ../references/performance-optimization.md for full guide
*/
/**
* ISSUE 4: Next.js 15 + React 19 Compatibility
*
* Status: Most issues resolved in latest Motion version
*
* Solution: Update to latest:
* pnpm add motion@latest react@latest next@latest
*
* If issues persist, check GitHub: https://github.com/motiondivision/motion/issues
*/
/**
* ISSUE 5: Reorder Component Doesn't Work
*
* Cause: Reorder component incompatible with Next.js routing
*
* Solution: Use alternative drag-to-reorder implementations or avoid Reorder
*
* GitHub issues: #2183, #2101
*/
// ============================================================================
// PERFORMANCE OPTIMIZATION
// ============================================================================
/**
* 1. Use "motion/react-client" import (not "motion/react")
* - Reduces client JS bundle
* - Optimized for Next.js App Router
*
* 2. Add willChange for transforms:
* <motion.div style={{ willChange: "transform" }} animate={{ x: 100 }} />
*
* 3. Use LazyMotion for smaller bundle (see Issue 3 above)
*
* 4. For large lists (50+ items), use virtualization:
* pnpm add @tanstack/react-virtual
*
* 5. Memoize expensive components:
* import { memo } from "react"
* export const AnimatedCard = memo(AnimatedCardComponent)
*/
// ============================================================================
// TYPESCRIPT TYPES
// ============================================================================
/**
* Motion includes full TypeScript support out of the box.
* No @types package needed.
*
* Common types:
*/
import type {
HTMLMotionProps,
SVGMotionProps,
Variants,
Target,
Transition,
MotionValue,
AnimationControls,
DragControls,
} from "motion/react-client"
// Example: Typed motion component props
interface AnimatedBoxProps extends HTMLMotionProps<"div"> {
title: string
}
export function AnimatedBox({ title, ...motionProps }: AnimatedBoxProps) {
return (
<motion.div {...motionProps}>
<h3>{title}</h3>
</motion.div>
)
}
// Example: Typed variants
const typedVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
/**
* QUICK REFERENCE
*
* App Router Requirements:
* ✅ Add "use client" to files using Motion
* ✅ Use "motion/react-client" import for optimized bundle
* ✅ Server Components can import Client Components
* ✅ Wrap app in MotionProvider for reduced motion support
* ❌ Don't use Motion in Server Components
* ❌ Don't rely on AnimatePresence for route transitions (unreliable)
*
* See ../references/nextjs-integration.md for comprehensive guide
*/
Related skills
How it compares
Use motion for Framer Motion-specific a11y patterns; use general frontend a11y skills for non-animated component audits.
FAQ
What accessibility settings does the motion skill address?
The motion skill addresses prefers-reduced-motion for users with vestibular disorders, keyboard-only navigation support, ARIA integration for screen readers, and explicit accessibility testing for Motion animated React components.
Why use prefers-reduced-motion with Motion?
prefers-reduced-motion lets Motion UIs honor OS Reduce Motion settings so users with motion sensitivity see minimal animation. The motion skill shows how to degrade animations instead of ignoring that system preference.
Is 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.