web-animation-framer-motion ยท diff
git:20260202.b236384 to git:20260316.00cb75b
147 added, 561 removed. Audit A to A.
---
name: web-animation-framer-motion
description: Motion (formerly Framer Motion) animation patterns - motion components, variants, gestures, layout animations, scroll-linked animations, accessibility
---
# Motion Animation Patterns
- > **Quick Guide:** Use Motion (formerly Framer Motion) for declarative React animations. motion components for basic animations, variants for orchestrated sequences, AnimatePresence for exit animations, layout/layoutId for FLIP animations, useScroll/useInView for scroll-triggered effects.
-
- > **v11+ Note:** The package has been renamed from `framer-motion` to `motion`. Import from `"motion/react"` instead of `"framer-motion"`. Both packages are supported during migration.
+ > **Quick Guide:** Use Motion for declarative React animations. `motion.*` components for basic animations, variants for orchestrated sequences, AnimatePresence for exit animations, `layout`/`layoutId` for FLIP animations, `useScroll`/`useInView` for scroll-triggered effects. Always animate transform properties (x, y, scale, rotate, opacity) for GPU performance. Always respect reduced motion via `MotionConfig reducedMotion="user"`.
- > **v12 Note (Current: v12.26+):** New features include `usePageInView` for background tab detection, enhanced `stagger()` with `from`/`ease` options, `resize()` function, and `.stop()`/`.cancel()` on drag controls.
+ > **Import:** `import { motion } from "motion/react"` (v11+ package rename from `framer-motion`)
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST wrap exiting components in AnimatePresence for exit animations to work)**
**(You MUST provide unique `key` prop to direct children of AnimatePresence)**
**(You MUST animate transform properties (x, y, scale, rotate, opacity) for GPU-accelerated performance)**
**(You MUST respect reduced motion preferences using MotionConfig or useReducedMotion)**
**(You MUST use named constants for all animation timing values - NO magic numbers)**
</critical_requirements>
---
**Auto-detection:** Motion, Framer Motion, motion.div, motion.button, AnimatePresence, useAnimation, useScroll, useInView, usePageInView, variants, whileHover, whileTap, layoutId, spring, tween, stagger, "motion/react", "framer-motion"
**When to use:**
- Animating component enter/exit/presence
- Orchestrating complex multi-element animations with variants
- Implementing gesture-based interactions (hover, tap, drag)
- Creating scroll-triggered or scroll-linked animations
- Animating layout changes and shared element transitions
- Building micro-interactions and UI feedback
+ **When NOT to use:**
+
+ - Simple CSS transitions (use CSS transitions instead)
+ - Complex timeline-based animations requiring frame-level control (consider a dedicated timeline animation library)
+ - Performance-critical animations on low-powered devices without careful optimization
+
**Key patterns covered:**
- motion components and animation props (initial, animate, exit, transition)
- Variants for reusable, orchestrated animations
- - AnimatePresence for exit animations
+ - AnimatePresence for exit animations and animation modes
- Gesture props (whileHover, whileTap, whileDrag, drag)
- - Layout animations (layout prop, layoutId)
+ - Layout animations (layout prop, layoutId, LayoutGroup)
- Scroll animations (useScroll, useInView, whileInView)
- Spring and tween transitions
- useAnimation for imperative control
- Reduced motion accessibility
- - v12: usePageInView, enhanced stagger(), resize()
-
- **When NOT to use:**
-
- - Simple CSS transitions (use CSS transitions instead)
- - Complex timeline-based animations requiring frame-level control (consider GSAP)
- - Performance-critical animations on low-powered devices without careful optimization
-
- **Package Migration (v11+):**
-
- ```bash
- # Migrate from framer-motion to motion
- npm uninstall framer-motion
- npm install motion
+ - v12: usePageInView, enhanced stagger(), drag stop/cancel
- # Update imports
- # Old: import { motion } from "framer-motion"
- # New: import { motion } from "motion/react"
- ```
+ ---
**Detailed Resources:**
- - For code examples, see [examples/](examples/) folder
- - For decision frameworks and anti-patterns, see [reference.md](reference.md)
+ - [examples/core.md](examples/core.md) - Motion components, variants, AnimatePresence, gestures, accessibility
+ - [examples/layout.md](examples/layout.md) - Layout animations, shared elements, expandable cards
+ - [examples/scroll.md](examples/scroll.md) - Scroll progress, reveal, parallax
+ - [examples/sequences.md](examples/sequences.md) - Complex sequences, keyframes
+ - [examples/svg.md](examples/svg.md) - SVG path animations
+ - [reference.md](reference.md) - Decision frameworks, migration guide, anti-patterns, quick reference
---
<philosophy>
## Philosophy
- Framer Motion is a declarative animation library for React that makes animations feel natural and accessible. It uses a physics-based approach with spring animations as defaults, creating fluid motion that matches real-world expectations.
+ Motion is a declarative animation library for React that makes animations feel natural and accessible. It uses a physics-based approach with spring animations as defaults, creating fluid motion that matches real-world expectations.
**Core principles:**
1. **Declarative over imperative** - Describe what the animation should look like, not how to achieve it
2. **Props over keyframes** - Use `initial`, `animate`, `exit` props instead of CSS keyframes
3. **Variants for orchestration** - Group related animations and control timing with parent-child relationships
4. **Performance through transforms** - Animate GPU-accelerated properties (transform, opacity) for smooth 60fps
5. **Accessibility built-in** - Respect user preferences for reduced motion
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Basic Motion Components
- Motion components are the foundation of Framer Motion. Prefix any HTML or SVG element with `motion.` to make it animatable.
-
- #### Animation Props
+ Prefix any HTML or SVG element with `motion.` to make it animatable. Use `initial`, `animate`, `exit`, and `transition` props.
```typescript
- // v11+ (recommended - motion package)
import { motion } from "motion/react";
- // Legacy (framer-motion package - still works during migration)
- // import { motion } from "framer-motion";
const FADE_DURATION_S = 0.3;
const SLIDE_DISTANCE_PX = 20;
- // Basic animation with named constants
- export const FadeIn = ({ children }: { children: React.ReactNode }) => {
- return (
- <motion.div
- initial={{ opacity: 0, y: SLIDE_DISTANCE_PX }}
- animate={{ opacity: 1, y: 0 }}
- transition={{ duration: FADE_DURATION_S }}
- >
- {children}
- </motion.div>
- );
- };
+ export const FadeIn = ({ children }: { children: React.ReactNode }) => (
+ <motion.div
+ initial={{ opacity: 0, y: SLIDE_DISTANCE_PX }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: FADE_DURATION_S }}
+ >
+ {children}
+ </motion.div>
+ );
```
- **Why good:** Named constants for timing values improve maintainability, declarative props clearly express animation intent, transform properties (y) are GPU-accelerated
-
- ```typescript
- // Bad Example - Magic numbers and layout properties
- export const FadeIn = ({ children }) => {
- return (
- <motion.div
- initial={{ opacity: 0, top: 20 }} // Bad: 'top' triggers layout
- animate={{ opacity: 1, top: 0 }}
- transition={{ duration: 0.3 }} // Bad: magic number
- >
- {children}
- </motion.div>
- );
- };
- ```
+ **Why good:** Named constants, declarative intent, `y` is GPU-accelerated (never animate `top`/`left`/`margin`)
- **Why bad:** Magic numbers make timing changes error-prone, animating `top` triggers expensive layout recalculations instead of using GPU-accelerated `y`
+ See [examples/core.md](examples/core.md) Pattern 1 for full examples with className, delay props, and bad examples.
---
### Pattern 2: Variants for Orchestrated Animations
- Variants define reusable animation states and enable parent-child orchestration with staggered timing.
-
- #### Defining Variants
+ Variants define reusable animation states and enable parent-child orchestration with `staggerChildren`.
```typescript
- import { motion, type Variants } from "framer-motion";
+ import { motion, type Variants } from "motion/react";
const STAGGER_DELAY_S = 0.1;
- const ITEM_SLIDE_DISTANCE_PX = 20;
- const ITEM_DURATION_S = 0.4;
const containerVariants: Variants = {
hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: STAGGER_DELAY_S,
- },
- },
+ visible: { opacity: 1, transition: { staggerChildren: STAGGER_DELAY_S } },
};
const itemVariants: Variants = {
- hidden: { opacity: 0, y: ITEM_SLIDE_DISTANCE_PX },
- visible: {
- opacity: 1,
- y: 0,
- transition: { duration: ITEM_DURATION_S },
- },
- };
-
- export const StaggeredList = ({ items }: { items: string[] }) => {
- return (
- <motion.ul
- variants={containerVariants}
- initial="hidden"
- animate="visible"
- >
- {items.map((item) => (
- <motion.li key={item} variants={itemVariants}>
- {item}
- </motion.li>
- ))}
- </motion.ul>
- );
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0 },
};
```
- **Why good:** Variants are reusable across components, staggerChildren creates natural cascading effect, children automatically inherit animation state from parent, named constants for all timing
+ Children automatically inherit animation state from parent. Use `staggerDirection: -1` for reverse stagger on exit.
- **When to use:** Lists with staggered entry, multi-step animations, coordinated UI transitions
+ See [examples/core.md](examples/core.md) Pattern 2 for complete list animation with exit variants.
---
### Pattern 3: AnimatePresence for Exit Animations
- AnimatePresence enables exit animations for components being removed from the React tree.
-
- #### Basic Exit Animation
-
- ```typescript
- import { AnimatePresence, motion } from "framer-motion";
-
- const MODAL_DURATION_S = 0.2;
- const MODAL_SCALE_HIDDEN = 0.95;
-
- type ModalProps = {
- isOpen: boolean;
- onClose: () => void;
- children: React.ReactNode;
- };
-
- export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
- return (
- <AnimatePresence>
- {isOpen && (
- <motion.div
- key="modal"
- initial={{ opacity: 0, scale: MODAL_SCALE_HIDDEN }}
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: MODAL_SCALE_HIDDEN }}
- transition={{ duration: MODAL_DURATION_S }}
- role="dialog"
- aria-modal="true"
- >
- {children}
- <button onClick={onClose} aria-label="Close modal">
- Close
- </button>
- </motion.div>
- )}
- </AnimatePresence>
- );
- };
- ```
-
- **Why good:** Exit animation plays before component unmounts, unique key ensures AnimatePresence tracks the element, scale and opacity are GPU-accelerated, accessibility attributes included
-
- #### Animation Modes
+ AnimatePresence enables exit animations for components being removed from the React tree. Direct children **must** have unique `key` props.
```typescript
- // mode="wait" - wait for exit before enter
- <AnimatePresence mode="wait">
- <motion.div key={currentPage}>
- {/* New page waits for old page to exit */}
- </motion.div>
- </AnimatePresence>
-
- // mode="sync" (default) - enter and exit simultaneously
- <AnimatePresence mode="sync">
- {/* Elements animate at the same time */}
- </AnimatePresence>
+ import { AnimatePresence, motion } from "motion/react";
- // mode="popLayout" - for shared layout transitions
- <AnimatePresence mode="popLayout">
- {/* Animates layout changes during exit */}
+ <AnimatePresence>
+ {isOpen && (
+ <motion.div
+ key="modal"
+ initial={{ opacity: 0, scale: 0.95 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.95 }}
+ />
+ )}
</AnimatePresence>
```
- **When to use:** Page transitions (mode="wait"), modals, toasts, conditional UI elements
+ **Animation modes:** `mode="sync"` (default, simultaneous), `mode="wait"` (wait for exit before enter - ideal for page transitions), `mode="popLayout"` (for shared layout transitions).
+ See [examples/core.md](examples/core.md) Pattern 3 for modal and page transition examples.
+
---
### Pattern 4: Gesture Animations
Gesture props enable hover, tap, focus, and drag interactions.
- #### Hover and Tap States
-
```typescript
- import { motion } from "framer-motion";
-
const HOVER_SCALE = 1.05;
const TAP_SCALE = 0.95;
const GESTURE_SPRING = { type: "spring" as const, stiffness: 400, damping: 17 };
- type ButtonProps = React.ComponentProps<typeof motion.button>;
-
- export const AnimatedButton = ({ children, ...props }: ButtonProps) => {
- return (
- <motion.button
- whileHover={{ scale: HOVER_SCALE }}
- whileTap={{ scale: TAP_SCALE }}
- transition={GESTURE_SPRING}
- {...props}
- >
- {children}
- </motion.button>
- );
- };
+ <motion.button
+ whileHover={{ scale: HOVER_SCALE }}
+ whileTap={{ scale: TAP_SCALE }}
+ transition={GESTURE_SPRING}
+ />
```
- **Why good:** Spring physics create natural bounce, scale transforms are GPU-accelerated, gesture states are declarative
-
- #### Drag Interactions
-
- ```typescript
- import { motion } from "framer-motion";
-
- const DRAG_ELASTIC = 0.2;
-
- export const DraggableCard = ({ children }: { children: React.ReactNode }) => {
- return (
- <motion.div
- drag
- dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
- dragElastic={DRAG_ELASTIC}
- whileDrag={{ scale: 1.1, cursor: "grabbing" }}
- >
- {children}
- </motion.div>
- );
- };
- ```
+ For drag: use `drag`, `dragConstraints`, `dragElastic`, `whileDrag`. Use `useDragControls` for programmatic drag (v12+ adds `.stop()`/`.cancel()`).
- **When to use:** Interactive buttons, draggable elements, swipeable cards, hover effects
+ See [examples/core.md](examples/core.md) Pattern 4 for interactive card and draggable element examples.
---
### Pattern 5: Layout Animations
- The `layout` prop animates layout changes automatically using FLIP technique.
-
- #### Basic Layout Animation
-
- ```typescript
- import { motion } from "framer-motion";
-
- const LAYOUT_SPRING = { type: "spring" as const, stiffness: 500, damping: 30 };
-
- type ExpandableCardProps = {
- isExpanded: boolean;
- onToggle: () => void;
- };
-
- export const ExpandableCard = ({ isExpanded, onToggle }: ExpandableCardProps) => {
- return (
- <motion.div
- layout
- transition={LAYOUT_SPRING}
- onClick={onToggle}
- style={{ width: isExpanded ? 300 : 150 }}
- >
- <motion.h2 layout="position">Title</motion.h2>
- {isExpanded && (
- <motion.p
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- >
- Expanded content here
- </motion.p>
- )}
- </motion.div>
- );
- };
- ```
-
- **Why good:** `layout` prop handles FLIP calculations automatically, `layout="position"` on children prevents distortion, spring transition feels natural
-
- #### Shared Element Transitions with layoutId
+ The `layout` prop animates layout changes automatically using FLIP technique. Use `layout="position"` on children to prevent text distortion. Use `layoutId` for shared element transitions across different containers.
```typescript
- import { motion, AnimatePresence } from "framer-motion";
-
- type TabsProps = {
- tabs: string[];
- activeTab: string;
- onTabChange: (tab: string) => void;
- };
+ <motion.div layout transition={LAYOUT_SPRING}>
+ <motion.h2 layout="position">Title</motion.h2>
+ </motion.div>
- export const Tabs = ({ tabs, activeTab, onTabChange }: TabsProps) => {
- return (
- <div role="tablist">
- {tabs.map((tab) => (
- <button
- key={tab}
- role="tab"
- aria-selected={activeTab === tab}
- onClick={() => onTabChange(tab)}
- >
- {tab}
- {activeTab === tab && (
- <motion.div
- layoutId="tab-underline"
- style={{ height: 2, background: "currentColor" }}
- transition={{ type: "spring", stiffness: 500, damping: 30 }}
- />
- )}
- </button>
- ))}
- </div>
- );
- };
+ // Shared element: layoutId creates seamless transitions
+ {activeTab === tab && <motion.div layoutId="indicator" />}
```
- **Why good:** `layoutId` creates seamless shared element transitions, underline smoothly slides between tabs, ARIA attributes for accessibility
+ Use `LayoutGroup` with `id` prop to scope `layoutId` to component instances (layoutId is global by default).
- **When to use:** Tabs, expanding cards, shared element transitions, list reordering
+ See [examples/layout.md](examples/layout.md) for expandable cards and tab indicator examples.
---
### Pattern 6: Scroll-Triggered Animations
- Use `whileInView` for scroll-triggered animations and `useScroll` for scroll-linked effects.
-
- #### Scroll-Triggered with whileInView
-
- ```typescript
- import { motion } from "framer-motion";
-
- const REVEAL_DURATION_S = 0.6;
- const REVEAL_DISTANCE_PX = 50;
-
- export const ScrollReveal = ({ children }: { children: React.ReactNode }) => {
- return (
- <motion.div
- initial={{ opacity: 0, y: REVEAL_DISTANCE_PX }}
- whileInView={{ opacity: 1, y: 0 }}
- viewport={{ once: true, margin: "-100px" }}
- transition={{ duration: REVEAL_DURATION_S }}
- >
- {children}
- </motion.div>
- );
- };
- ```
-
- **Why good:** `viewport.once` ensures animation only plays once, negative margin triggers animation before element fully enters viewport, declarative scroll trigger
-
- #### Scroll-Linked with useScroll
+ `whileInView` for scroll-triggered animations. `useScroll` + `useTransform` for scroll-linked effects.
```typescript
- import { motion, useScroll, useTransform } from "framer-motion";
- import { useRef } from "react";
-
- const PARALLAX_RANGE = [-100, 100];
-
- export const ParallaxSection = ({ children }: { children: React.ReactNode }) => {
- const ref = useRef<HTMLDivElement>(null);
- const { scrollYProgress } = useScroll({
- target: ref,
- offset: ["start end", "end start"],
- });
-
- const y = useTransform(scrollYProgress, [0, 1], PARALLAX_RANGE);
+ // Scroll-triggered (fires once)
+ <motion.div
+ initial={{ opacity: 0, y: 50 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ viewport={{ once: true, margin: "-100px" }}
+ />
- return (
- <div ref={ref}>
- <motion.div style={{ y }}>
- {children}
- </motion.div>
- </div>
- );
- };
+ // Scroll-linked (continuous)
+ const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] });
+ const y = useTransform(scrollYProgress, [0, 1], [-100, 100]);
```
- **Why good:** `useTransform` maps scroll progress to animation values, offset controls when tracking starts/ends, motion values update without React re-renders
-
- #### useInView for Custom Logic
-
- ```typescript
- import { useInView } from "framer-motion";
- import { useRef, useEffect } from "react";
-
- export const LazyVideo = ({ src }: { src: string }) => {
- const ref = useRef<HTMLVideoElement>(null);
- const isInView = useInView(ref, { once: true });
-
- useEffect(() => {
- if (isInView && ref.current) {
- ref.current.play();
- }
- }, [isInView]);
-
- return <video ref={ref} src={src} />;
- };
- ```
+ Motion values from `useScroll` update without React re-renders.
- **When to use:** Scroll reveal effects, parallax, progress indicators, lazy loading
+ See [examples/scroll.md](examples/scroll.md) for progress bar, parallax, and reveal examples.
---
### Pattern 7: Spring and Tween Transitions
- Configure transition physics for different animation feels.
-
- #### Spring Transitions
-
```typescript
- import { motion } from "framer-motion";
-
- // Bouncy spring for playful interactions
- const BOUNCY_SPRING = {
- type: "spring" as const,
- stiffness: 300,
- damping: 10,
- };
-
- // Snappy spring for responsive UI
- const SNAPPY_SPRING = {
- type: "spring" as const,
- stiffness: 500,
- damping: 30,
- };
+ // Springs - physics-based, natural feel
+ const BOUNCY = { type: "spring", stiffness: 300, damping: 10 }; // Playful
+ const SNAPPY = { type: "spring", stiffness: 500, damping: 30 }; // Responsive
+ const GENTLE = { type: "spring", stiffness: 100, damping: 20 }; // Subtle
- // Gentle spring for subtle movements
- const GENTLE_SPRING = {
- type: "spring" as const,
- stiffness: 100,
- damping: 20,
- };
+ // Tweens - duration-based, precise control
+ const ENTER = { type: "tween", ease: "easeOut", duration: 0.3 }; // Enter
+ const EXIT = { type: "tween", ease: "easeIn", duration: 0.2 }; // Exit
```
- #### Tween Transitions
-
- ```typescript
- // Linear for constant speed
- const LINEAR_TWEEN = {
- type: "tween" as const,
- duration: 0.3,
- ease: "linear",
- };
-
- // Ease out for entering elements
- const EASE_OUT_TWEEN = {
- type: "tween" as const,
- duration: 0.3,
- ease: "easeOut",
- };
-
- // Ease in for exiting elements
- const EASE_IN_TWEEN = {
- type: "tween" as const,
- duration: 0.2,
- ease: "easeIn",
- };
-
- // Custom cubic bezier
- const CUSTOM_TWEEN = {
- type: "tween" as const,
- duration: 0.4,
- ease: [0.25, 0.1, 0.25, 1],
- };
- ```
+ **Rule of thumb:** Springs for interactive elements (buttons, cards), tweens for UI transitions (modals, page changes).
- **When to use:** Springs for interactive elements (buttons, cards), tweens for UI transitions (modals, page changes)
+ See [reference.md](reference.md) for full transition type reference with additional presets.
---
### Pattern 8: useAnimation for Imperative Control
- Use `useAnimation` when you need programmatic control over animations.
+ Use `useAnimation` when you need programmatic control over animations triggered by external events, complex sequences, or start/stop behavior.
```typescript
- import { motion, useAnimation } from "framer-motion";
- import { useEffect } from "react";
-
- const SHAKE_DISTANCE_PX = 10;
- const SHAKE_DURATION_S = 0.1;
- const SHAKE_TIMES = 3;
-
- export const ShakeOnError = ({
- hasError,
- children,
- }: {
- hasError: boolean;
- children: React.ReactNode;
- }) => {
- const controls = useAnimation();
+ const controls = useAnimation();
- useEffect(() => {
- if (hasError) {
- controls.start({
- x: [0, -SHAKE_DISTANCE_PX, SHAKE_DISTANCE_PX, -SHAKE_DISTANCE_PX, 0],
- transition: { duration: SHAKE_DURATION_S * SHAKE_TIMES },
- });
- }
- }, [hasError, controls]);
+ useEffect(() => {
+ if (hasError) {
+ controls.start({
+ x: [0, -10, 10, -10, 0],
+ transition: { duration: 0.3 },
+ });
+ }
+ }, [hasError, controls]);
- return <motion.div animate={controls}>{children}</motion.div>;
- };
+ <motion.div animate={controls}>{children}</motion.div>
```
- **Why good:** Imperative control for complex sequencing, keyframe array creates shake effect, effect triggers on error state change
-
- **When to use:** Animations triggered by external events, complex sequences, animations that need to be started/stopped programmatically
+ See [examples/sequences.md](examples/sequences.md) for multi-step sequences and keyframe animations.
---
### Pattern 9: Reduced Motion Accessibility
Always respect user preferences for reduced motion.
- #### Site-Wide with MotionConfig
-
```typescript
- import { MotionConfig } from "framer-motion";
-
- export const App = ({ children }: { children: React.ReactNode }) => {
- return (
- <MotionConfig reducedMotion="user">
- {children}
- </MotionConfig>
- );
- };
- ```
-
- **Why good:** Automatically disables transform/layout animations when reduced motion is preferred, opacity and color animations still work
-
- #### Custom Handling with useReducedMotion
-
- ```typescript
- import { motion, useReducedMotion } from "framer-motion";
-
- const SLIDE_DISTANCE_PX = 50;
- const SLIDE_DURATION_S = 0.5;
- const FADE_DURATION_S = 0.3;
-
- export const AccessibleReveal = ({ children }: { children: React.ReactNode }) => {
- const shouldReduceMotion = useReducedMotion();
+ // Site-wide: wrap app root
+ <MotionConfig reducedMotion="user">{children}</MotionConfig>
- return (
- <motion.div
- initial={{ opacity: 0, y: shouldReduceMotion ? 0 : SLIDE_DISTANCE_PX }}
- animate={{ opacity: 1, y: 0 }}
- transition={{
- duration: shouldReduceMotion ? FADE_DURATION_S : SLIDE_DURATION_S,
- }}
- >
- {children}
- </motion.div>
- );
- };
+ // Per-component: custom handling
+ const shouldReduceMotion = useReducedMotion();
+ <motion.div
+ initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 50 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: shouldReduceMotion ? 0.2 : 0.5 }}
+ />
```
- **Why good:** Provides graceful degradation instead of no animation, maintains visual feedback while respecting preferences, hook reactively updates if setting changes
+ `MotionConfig reducedMotion="user"` automatically disables transform/layout animations when reduced motion is preferred. Opacity and color animations still work.
- **When to use:** Always - reduced motion support is a requirement for accessible applications
+ See [examples/core.md](examples/core.md) Pattern 5 for complete accessible animation component.
---
### Pattern 10: v12 Features
- New features introduced in Motion v12.
-
- #### usePageInView Hook (v12.19+)
-
- Detect when the page/tab is visible to pause animations or videos in background tabs.
+ **usePageInView** (v12.19+): Detect when page/tab is visible to pause animations or videos in background tabs. Returns `boolean`, defaults to `true` on server.
```typescript
import { usePageInView } from "motion/react";
- import { useRef, useEffect } from "react";
-
- export const AutoPausingVideo = ({ src }: { src: string }) => {
- const videoRef = useRef<HTMLVideoElement>(null);
- const isPageVisible = usePageInView();
-
- useEffect(() => {
- if (!videoRef.current) return;
-
- if (isPageVisible) {
- videoRef.current.play();
- } else {
- videoRef.current.pause();
- }
- }, [isPageVisible]);
-
- return <video ref={videoRef} src={src} muted loop />;
- };
+ const isPageVisible = usePageInView();
```
- **Why good:** Automatically pauses resource-intensive animations when user switches tabs, improves performance and battery life
-
- #### Enhanced stagger() with from and ease (v12+)
-
- The `stagger()` function now supports staggering from any child and eased staggering.
+ **Enhanced stagger()** (v12+): Pass `stagger()` to `delayChildren` in variants for `from` and `ease` options.
```typescript
- import { motion, stagger, type Variants } from "motion/react";
-
- const STAGGER_DELAY_S = 0.05;
-
- const containerVariants: Variants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- // Stagger from center with easing
- staggerChildren: stagger(STAGGER_DELAY_S, {
- from: "center",
- ease: "easeOut",
- }),
- },
- },
- };
+ import { stagger } from "motion/react";
- // Alternative: stagger from a specific index
- const fromIndexVariants: Variants = {
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: stagger(STAGGER_DELAY_S, {
- from: 3, // Start from 4th child (0-indexed)
- }),
- },
- },
+ // stagger() is passed to delayChildren, NOT staggerChildren
+ const transition = {
+ delayChildren: stagger(0.05, { from: "center", ease: "easeOut" }),
};
-
- // Options: "first" (default), "center", "last", or number (index)
+ // from options: "first" (default), "center", "last", or number (index)
```
- **Why good:** Enables ripple-out effects from center or specific elements, eased staggering creates more polished animations
-
- #### Drag Controls with stop/cancel (v12+)
-
- ```typescript
- import { motion, useDragControls } from "motion/react";
-
- export const CancellableDrag = () => {
- const controls = useDragControls();
-
- const handleEmergencyStop = () => {
- controls.stop(); // Stop drag immediately
- // or controls.cancel(); // Cancel and revert
- };
-
- return (
- <motion.div
- drag
- dragControls={controls}
- onDragEnd={() => console.log("Drag ended")}
- >
- Drag me
- </motion.div>
- );
- };
- ```
+ **Drag Controls** (v12+): `useDragControls` gains `.stop()` and `.cancel()` methods.
- **When to use:** When you need programmatic control to stop or cancel drag operations
+ **MotionConfig skipAnimations**: Globally disable animations for testing or during complex transitions.
</patterns>
---
- <integration>
+ <red_flags>
- ## Integration Guide
+ ## RED FLAGS
- **Framer Motion is React-specific.** It provides animation primitives that work with any React component architecture and styling solution.
+ **High Priority Issues:**
- **Works with:**
+ - Missing AnimatePresence for exit animations - exit prop has no effect without it
+ - Missing unique key on AnimatePresence children - cannot track elements
+ - Animating layout-triggering properties (height, width, top, left, margin, padding) - use transform (x, y, scale) instead
+ - Magic numbers for timing values - all durations, delays, distances must be named constants
+ - Ignoring reduced motion - always use `MotionConfig reducedMotion="user"` or `useReducedMotion`
- - **React components**: Wrap any component with motion() or use motion.element
- - **Any styling solution**: Apply styles via className prop, motion handles animation
- - **Form libraries**: Animate form validation states, error messages
- - **Routing solutions**: Use AnimatePresence for page transitions
+ **Medium Priority Issues:**
- **Animation state is separate from React state:**
+ - Using index as key in animated lists - causes incorrect animations when list changes
+ - Missing `layout="position"` on children during parent layout animation - children will distort
+ - Overusing `willChange` - creates GPU layers; Motion handles optimization automatically
+ - Not cleaning up `useAnimation` in useEffect - can cause memory leaks
- - Motion values update without React re-renders
- - Use `useAnimation` for imperative control from effects
- - Variants propagate animation state to children automatically
+ **Gotchas & Edge Cases:**
- </integration>
+ - `layoutId` is global - use LayoutGroup with id prop to scope to component instances
+ - `AnimatePresence mode="wait"` blocks enter until exit completes - may cause perceived delay
+ - `whileInView` uses `viewport` not `offset` for configuration (unlike `useScroll`)
+ - SVG animations require `motion.path`, `motion.circle`, etc. - regular SVG elements won't animate
+ - `useInView` returns `false` on server - default to visible state for SSR
+ - Motion values don't trigger re-renders (by design) - use `useMotionValueEvent` for side effects
+ - React Fragments inside AnimatePresence break tracking - each direct child must have a key
+ - `drag` with `layout` can conflict - disable layout during drag or use `dragListener`
+ - Spring animations can overshoot - high stiffness + low damping; test with real content
+ - v12 `stagger()` goes on `delayChildren`, not `staggerChildren` - they serve different purposes
+
+ </red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST wrap exiting components in AnimatePresence for exit animations to work)**
**(You MUST provide unique `key` prop to direct children of AnimatePresence)**
**(You MUST animate transform properties (x, y, scale, rotate, opacity) for GPU-accelerated performance)**
**(You MUST respect reduced motion preferences using MotionConfig or useReducedMotion)**
**(You MUST use named constants for all animation timing values - NO magic numbers)**
**Failure to follow these rules will break exit animations, cause performance issues, and create inaccessible experiences.**
</critical_reminders>