Contents

Chapter 1

Animation Guide

A practical guide to building accessible, performant animations with Framer Motion. Covers when to animate, what to animate, performance budgets, accessibility requirements, and debugging techniques.

Table of Contents

1. When to Animate

2. Choosing the Right Pattern

3. Performance Budget

4. Accessibility Requirements

5. Spring vs. Tween

6. Common Mistakes

7. Debugging Animations

8. Browser Compatibility


When to Animate

Animate When...

SituationWhyPattern
Content enters the viewportDraws attention to new contentScrollReveal with fadeUp
User navigates between pagesProvides spatial continuityPageTransition with slide
Data loads asynchronouslyReduces perceived wait timeSkeletonLoaderContentSwap
User interacts with an elementConfirms the interaction was receivedwhileHover + whileTap
A value changesShows the change, not just the resultAnimatedCounter
Items are added/removedPrevents jarring layout shiftsAnimatePresence + exit variants
State togglesShows the transition between stateslayout animation

Don't Animate When...

SituationWhy
The user is typingAnimations near text input are distracting
Form validation errors appearUsers need to read errors, not watch them bounce
Data is already visibleRe-animating visible content feels like lag
Multiple elements need attentionToo many simultaneous animations cause chaos
The action is destructiveDelete confirmations should feel deliberate, not flashy
Network requests are in-flightDon't animate loading states that may flash briefly

The 3-Second Rule

If an animation takes longer than 3 seconds to complete (including stagger), it's too long. Users lose patience after 3 seconds. The ideal animation:

  • Micro-interaction: 150-300ms
  • Entrance: 300-600ms
  • Page transition: 300-500ms
  • Stagger total: Under 500ms
  • Loading skeleton: Until data arrives (but with calm animation)

Choosing the Right Pattern

Decision Tree

Is the element entering the viewport for the first time?
├── YES → Is it triggered by scroll?
│   ├── YES → ScrollReveal component
│   └── NO → Entrance variant (fadeUp, scaleUp, etc.)
└── NO → Is the element responding to user interaction?
    ├── YES → Is it a hover/tap?
    │   ├── YES → Hover variant (lift, press, grow, etc.)
    │   └── NO → useMicroInteraction hook
    └── NO → Is the element leaving?
        ├── YES → Exit variant + AnimatePresence
        └── NO → Is it a layout change?
            ├── YES → layout prop + LayoutGroup
            └── NO → You probably don't need animation here

Pattern Recommendations by Context

ContextRecommended PatternAvoid
Marketing landing pageScrollReveal, Parallax, StaggerListOver-animating — let content breathe
Dashboard / data appPageTransition, AnimatedCounter, SkeletonLoaderParallax (too playful for data UI)
E-commercefadeUp on products, lift on cards, ContentSwapSlow transitions (users are goal-oriented)
Blog / documentationScrollReveal (subtle), fadeUp onlyStagger (text should appear all at once)
Mobile appReduced distances, faster springs, press over liftParallax (scroll jank risk on mobile)
Portfolio / creativeblurIn, clipReveal, morph page transitionsConservative patterns (this is where you show off)

Performance Budget

The 16ms Frame Budget

At 60fps, each frame has 16.67ms. Your animation logic must complete within this budget:

16ms total frame budget
├── 2-4ms   JavaScript (animation calculations)
├── 2-4ms   Style recalculation
├── 2-4ms   Layout (if properties change layout)
├── 2-4ms   Paint (if properties need repainting)
└── 2-4ms   Compositing (GPU layer management)

Properties by Cost

TierPropertiesCostGPU Composited?
Freetransform (translate, scale, rotate)Composite onlyYes
FreeopacityComposite onlyYes
Cheapfilter (blur, brightness)Paint + CompositePartially
Expensivebox-shadowPaint + CompositeNo
Very Expensivewidth, height, padding, marginLayout + Paint + CompositeNo
Catastrophictop, left, right, bottomLayout + Paint + CompositeNo

Rules

1. Only animate transform and opacity for the smoothest results

2. Use will-change: transform to hint the browser (the variants in this pack do this automatically)

3. Limit will-change to 3-5 elements — too many promoted layers waste GPU memory

4. Avoid animating during scroll unless using useScroll() (passive, RAF-based)

5. Test on low-end devices — your MacBook Pro is not representative

Measuring Performance

js
// In Chrome DevTools:
// 1. Open Performance panel
// 2. Enable "Screenshots" and "Web Vitals"
// 3. Click Record, trigger your animation, stop
// 4. Look for:
//    - Long frames (red bars above 16ms)
//    - Layout shifts (yellow "Layout" blocks)
//    - Paint events (green blocks — should be rare during animation)

// Programmatically:
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 16) {
      console.warn(`Long frame: ${entry.duration.toFixed(1)}ms`, entry);
    }
  }
});
observer.observe({ type: 'longtask', buffered: true });

Accessibility Requirements

prefers-reduced-motion

This is not optional. Approximately 25-35% of users have motion sensitivity to some degree. Vestibular disorders can cause nausea, dizziness, and migraines from screen motion.

Every animation in this pack respects prefers-reduced-motion: reduce automatically. Here's what happens:

Full MotionReduced Motion
Slide + fade entranceOpacity-only fade (200ms)
Parallax scroll effectsStatic content (no movement)
Staggered list animationQuick sequential opacity fade
Spring-based hoverOpacity change (no transform)
Page slide transitionCrossfade only
Animated counterShows final value immediately
Shimmer skeletonSlow pulse (no directional movement)

Testing Reduced Motion

macOS: System Settings → Accessibility → Display → Reduce motion

Windows: Settings → Ease of Access → Display → Show animations → Off

Chrome DevTools: Rendering → Emulate CSS media feature → prefers-reduced-motion: reduce

WCAG Compliance

  • 2.3.1 Three Flashes: No content flashes more than 3 times per second
  • 2.3.3 Animation from Interactions: Provide mechanism to disable non-essential animation
  • 2.2.2 Pause, Stop, Hide: Moving content can be paused by the user

The ReducedMotionProvider in this pack satisfies all three criteria when wrapped around your app root.


Spring vs. Tween

When to Use Springs

Springs use physics simulation (stiffness, damping, mass) instead of fixed duration + easing curve.

Use springs for:

  • Anything the user interacts with (buttons, cards, toggles)
  • Layout animations (things moving to new positions)
  • Draggable elements
  • Any animation that might be interrupted mid-motion

Why: Springs handle interruption gracefully. If a spring animation is in-flight and you change the target, it smoothly redirects. Tweens jump to the start of the new animation.

When to Use Tweens

Tweens use fixed duration + easing curve (like CSS transitions).

Use tweens for:

  • Decorative animations (shimmer, gradient shifts)
  • Clip-path reveals (springs don't work well with clip-path)
  • Filter animations (blur, brightness)
  • Anything with a fixed, predictable duration
  • Sequenced animations where timing must be exact

Spring Parameter Cheat Sheet

FeelStiffnessDampingMassDuration (approx)
Snappy (buttons)400301.0~200ms
Bouncy (playful)400150.8~400ms
Gentle (pages)120201.2~600ms
Stiff (instant)600350.5~100ms
Heavy (dramatic)100252.0~800ms
Wobbly (fun)350100.8~600ms

Use springFromBehavior() from easing.ts to calculate these from desired duration and bounce.


Common Mistakes

1. Animating Layout Properties

tsx
// BAD: Animating width causes layout thrashing
<motion.div animate={{ width: isOpen ? 300 : 0 }} />

// GOOD: Animate scaleX instead (GPU composited)
<motion.div animate={{ scaleX: isOpen ? 1 : 0 }} style={{ transformOrigin: 'left' }} />

// GOOD: If you must animate width, use layout animation
<motion.div layout style={{ width: isOpen ? 300 : 0 }} />

2. Too Many Simultaneous Animations

tsx
// BAD: Every card animates independently on mount
{cards.map(card => (
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
    {card.title}
  </motion.div>
))}

// GOOD: Use stagger so they animate sequentially
<StaggerList items={cards} staggerDelay={0.06} renderItem={...} />

3. Forgetting AnimatePresence

tsx
// BAD: Element unmounts instantly — exit animation never plays
{isVisible && <motion.div exit={{ opacity: 0 }}>Content</motion.div>}

// GOOD: AnimatePresence keeps element mounted during exit animation
<AnimatePresence>
  {isVisible && (
    <motion.div key="content" exit={{ opacity: 0 }}>Content</motion.div>
  )}
</AnimatePresence>

4. Missing key prop in AnimatePresence

tsx
// BAD: No key — AnimatePresence can't track which element is leaving
<AnimatePresence>
  {items.map(item => <motion.div>{item.name}</motion.div>)}
</AnimatePresence>

// GOOD: Unique key lets AnimatePresence track each element
<AnimatePresence>
  {items.map(item => (
    <motion.div key={item.id}>{item.name}</motion.div>
  ))}
</AnimatePresence>

5. Ignoring Reduced Motion

tsx
// BAD: No reduced motion consideration
<motion.div animate={{ x: 100, rotate: 360 }} />

// GOOD: Use variants from this pack — they include reduced fallbacks
import { fadeUp } from './variants/entrance';
const prefersReduced = useReducedMotion();
<motion.div {...(prefersReduced ? fadeUp.reduced : fadeUp)} />

Debugging Animations

Chrome DevTools Techniques

1. Slow down animations: Rendering → "Animation slow-down" slider

2. See layer composition: Layers panel → shows which elements are GPU-composited

3. Detect layout shifts: Performance panel → look for yellow "Layout" events during animation

4. Monitor paint: Rendering → "Paint flashing" → green highlights show repaints

Framer Motion Dev Tools

tsx
// Log animation lifecycle
<motion.div
  onAnimationStart={() => console.log('Animation started')}
  onAnimationComplete={() => console.log('Animation completed')}
  animate={{ opacity: 1 }}
/>

// Inspect motion values
import { useMotionValue, useTransform } from 'framer-motion';
const x = useMotionValue(0);
x.on('change', (latest) => console.log('x:', latest));

Common Issues & Solutions

SymptomCauseFix
Animation stutters on first runBrowser creating new GPU layerAdd will-change: transform to the element's idle state
Exit animation doesn't playMissing AnimatePresence wrapperWrap conditional rendering in
Stagger feels sluggishToo many items with default delayUse safeStaggerDelay() to cap total time
Spring oscillates too longDamping too lowIncrease damping or use restDelta option
Animation replays on every renderinitial prop without keyAdd stable key prop or use initial={false}
Layout shift when element appearsElement has fixed dimensionsUse layout prop for smooth size transitions

Browser Compatibility

FeatureChromeFirefoxSafariEdge
CSS transforms36+16+9+12+
Intersection Observer51+55+12.1+15+
prefers-reduced-motion74+63+10.1+79+
Web Animations API36+48+13.1+79+
will-change36+36+9.1+79+
filter (blur, etc.)53+35+9.1+79+
clip-path (inset)55+54+13.1+79+

All features used in this pack are supported in all modern browsers (released after 2020).

For IE11 support (not recommended), you would need:

  • Intersection Observer polyfill
  • Web Animations API polyfill
  • CSS custom properties polyfill
Animation & Effects Pack v1.0.0 — Free Preview