web-animation-framer-motion · git:20260202.b236384 · 2026-02-02 · sha256 57495f8edf66ea61

web-animation-framer-motion git:20260202.b236384A

Immutable. This exact content is served forever at /api/v1/blob/57495f8edf66ea61.

---
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.

> **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.

---

<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

**Key patterns covered:**

- motion components and animation props (initial, animate, exit, transition)
- Variants for reusable, orchestrated animations
- AnimatePresence for exit animations
- Gesture props (whileHover, whileTap, whileDrag, drag)
- Layout animations (layout prop, layoutId)
- 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

# 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)

---

<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.

**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

```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>
  );
};
```

**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 bad:** Magic numbers make timing changes error-prone, animating `top` triggers expensive layout recalculations instead of using GPU-accelerated `y`

---

### Pattern 2: Variants for Orchestrated Animations

Variants define reusable animation states and enable parent-child orchestration with staggered timing.

#### Defining Variants

```typescript
import { motion, type Variants } from "framer-motion";

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,
    },
  },
};

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>
  );
};
```

**Why good:** Variants are reusable across components, staggerChildren creates natural cascading effect, children automatically inherit animation state from parent, named constants for all timing

**When to use:** Lists with staggered entry, multi-step animations, coordinated UI transitions

---

### 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

```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>

// mode="popLayout" - for shared layout transitions
<AnimatePresence mode="popLayout">
  {/* Animates layout changes during exit */}
</AnimatePresence>
```

**When to use:** Page transitions (mode="wait"), modals, toasts, conditional UI elements

---

### 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>
  );
};
```

**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>
  );
};
```

**When to use:** Interactive buttons, draggable elements, swipeable cards, hover effects

---

### 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

```typescript
import { motion, AnimatePresence } from "framer-motion";

type TabsProps = {
  tabs: string[];
  activeTab: string;
  onTabChange: (tab: string) => void;
};

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>
  );
};
```

**Why good:** `layoutId` creates seamless shared element transitions, underline smoothly slides between tabs, ARIA attributes for accessibility

**When to use:** Tabs, expanding cards, shared element transitions, list reordering

---

### 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

```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);

  return (
    <div ref={ref}>
      <motion.div style={{ y }}>
        {children}
      </motion.div>
    </div>
  );
};
```

**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} />;
};
```

**When to use:** Scroll reveal effects, parallax, progress indicators, lazy loading

---

### 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,
};

// Gentle spring for subtle movements
const GENTLE_SPRING = {
  type: "spring" as const,
  stiffness: 100,
  damping: 20,
};
```

#### 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],
};
```

**When to use:** Springs for interactive elements (buttons, cards), tweens for UI transitions (modals, page changes)

---

### Pattern 8: useAnimation for Imperative Control

Use `useAnimation` when you need programmatic control over animations.

```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();

  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]);

  return <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

---

### 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();

  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>
  );
};
```

**Why good:** Provides graceful degradation instead of no animation, maintains visual feedback while respecting preferences, hook reactively updates if setting changes

**When to use:** Always - reduced motion support is a requirement for accessible applications

---

### 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.

```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 />;
};
```

**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.

```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",
      }),
    },
  },
};

// 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)
      }),
    },
  },
};

// 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>
  );
};
```

**When to use:** When you need programmatic control to stop or cancel drag operations

</patterns>

---

<integration>

## Integration Guide

**Framer Motion is React-specific.** It provides animation primitives that work with any React component architecture and styling solution.

**Works with:**

- **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

**Animation state is separate from React state:**

- Motion values update without React re-renders
- Use `useAnimation` for imperative control from effects
- Variants propagate animation state to children automatically

</integration>

---

<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>