Motion & Animation
Complete guide to web animation: principles for quality, performance rules for smoothness, and implementation patterns for common interactions.
Part A: Animation Principles (Disney's 12 for Web)
Disney animators codified these in the 1930s. We use them to make pixels feel human.
| # |
Principle |
Web Application |
| 1 |
Squash & Stretch |
Subtle deformation conveys weight in morphing elements — don't overdo it |
| 2 |
Anticipation |
Cues before action (pull-to-refresh hints, button compress before send) |
| 3 |
Staging |
Guide eye through sequential animation; dim backgrounds to focus |
| 4 |
Straight Ahead & Pose to Pose |
Define key poses (start, end, maybe midpoint); let browser interpolate |
| 5 |
Follow Through & Overlapping |
Springs add organic overshoot-and-settle; too much stagger feels slow |
| 6 |
Slow In & Slow Out |
ease-out for entrances, ease-in for exits, ease-in-out for deliberate |
| 7 |
Arcs |
Curved paths feel natural; best for hero moments and playful interactions |
| 8 |
Secondary Action |
Flourishes supporting the main action (sparkles, sound, particles) |
| 9 |
Timing |
Keep interactions <300ms; be consistent across similar elements |
| 10 |
Exaggeration |
Push past accuracy sparingly (onboarding, empty states, confirmations) |
| 11 |
Solid Drawing |
Shadows suggest depth; CSS perspective gives real 3D |
| 12 |
Appeal |
The sum of all techniques applied with care and taste |
Key Guidelines
- Balance: Too much animation turns professional software into a cartoon
- Consistency: Define timing scales early, reuse everywhere
- Purpose: Great animation is invisible — users think "this feels good"
- Restraint: Not everything needs to be animated
Part B: Animation Performance Rules
Rendering Pipeline
- Composite (cheapest):
transform, opacity
- Paint: color, borders, gradients, masks, images, filters
- Layout (most expensive): size, position, flow, grid, flex
Rule Categories by Priority
| Priority |
Category |
Impact |
| 1 |
Never Patterns |
CRITICAL |
| 2 |
Choose the Mechanism |
CRITICAL |
| 3 |
Measurement |
HIGH |
| 4 |
Scroll |
HIGH |
| 5 |
Paint |
MEDIUM-HIGH |
| 6 |
Layers |
MEDIUM |
| 7 |
Blur & Filters |
MEDIUM |
| 8 |
View Transitions |
LOW |
| 9 |
Tool Boundaries |
CRITICAL |
1. Never Patterns (CRITICAL)
- Do not interleave layout reads and writes in the same frame
- Do not animate layout continuously on large surfaces
- Do not drive animation from
scrollTop, scrollY, or scroll events
- No
requestAnimationFrame loops without a stop condition
- Do not mix multiple animation systems that each measure or mutate layout
2. Choose the Mechanism (CRITICAL)
- Default to
transform and opacity for motion
- JS-driven animation only when interaction requires it
- Paint/layout animation acceptable only on small, isolated surfaces
- One-shot effects acceptable more often than continuous motion
- Prefer downgrading technique over removing motion entirely
3. Measurement (HIGH)
- Measure once, then animate via transform or opacity
- Batch all DOM reads before writes
- Do not read layout repeatedly during an animation
- Prefer FLIP-style transitions for layout-like effects
4. Scroll (HIGH)
- Prefer Scroll/View Timelines for scroll-linked motion
- Use
IntersectionObserver for visibility and pausing
- Do not poll scroll position for animation
- Pause or stop animations when off-screen
5. Paint (MEDIUM-HIGH)
- Paint-triggering animation allowed only on small, isolated elements
- Do not animate CSS variables for transform, opacity, or position
- Scope animated CSS variables locally; avoid inheritance
6. Layers (MEDIUM)
- Compositor motion requires layer promotion — never assume it
- Use
will-change temporarily and surgically
- Avoid many or large promoted layers
7. Blur & Filters (MEDIUM)
- Keep blur ≤8px; use only for short, one-time effects
- Never animate blur continuously or on large surfaces
- Prefer opacity and translate before blur
8. View Transitions (LOW)
- Use only for navigation-level changes
- Avoid for interaction-heavy UI or when interruption is needed
9. Tool Boundaries (CRITICAL)
- Do not migrate animation libraries unless explicitly requested
- Apply rules within the existing animation system
Part C: Interaction Design Patterns
Timing Guidelines
| Duration |
Use Case |
| 100-150ms |
Micro-feedback (hovers, clicks) |
| 200-300ms |
Small transitions (toggles, dropdowns) |
| 300-500ms |
Medium transitions (modals, page changes) |
| 500ms+ |
Complex choreographed animations |
Easing Functions
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Playful overshoot */
Loading States
Skeleton screens (preserve layout):
function CardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-48 bg-gray-200 rounded-lg" />
<div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
</div>
);
}
Progress bar:
function ProgressBar({ progress }: { progress: number }) {
return (
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ ease: "easeOut" }}
/>
</div>
);
}
State Transitions
Toggle with spring:
function Toggle({ checked, onChange }) {
return (
<button
role="switch" aria-checked={checked}
=> onChange(!checked)}
className={`relative w-12 h-6 rounded-full transition-colors duration-200
${checked ? "bg-blue-600" : "bg-gray-300"}`}
>
<motion.span
className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
animate={{ x: checked ? 24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
);
}
Page Transitions
import { AnimatePresence, motion } from "framer-motion";
function PageTransition({ children, key }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={key}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}
Gesture Interactions
Swipe to dismiss:
function SwipeCard({ children, onDismiss }) {
return (
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
info) => {
if (Math.abs(info.offset.x) > 100) onDismiss();
}}
className="cursor-grab active:cursor-grabbing"
>
{children}
</motion.div>
);
}
CSS Animation Patterns
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
Accessibility
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Checklist
Quality
Performance
Accessibility
Implementation
References
1---2name: motion-and-animation3description: Comprehensive motion and animation guide — Disney's 12 principles for web, animation performance rules (compositor-only, measurement, scroll), and interaction design patterns (microinteractions, transitions, loading states, gestures). Use when implementing, reviewing, or fixing UI animations, transitions, or motion.4---56# Motion & Animation78Complete guide to web animation: principles for quality, performance rules for smoothness, and implementation patterns for common interactions.910---1112## Part A: Animation Principles (Disney's 12 for Web)1314Disney animators codified these in the 1930s. We use them to make pixels feel human.1516| # | Principle | Web Application |17|---|-----------|-----------------|18| 1 | **Squash & Stretch** | Subtle deformation conveys weight in morphing elements — don't overdo it |19| 2 | **Anticipation** | Cues before action (pull-to-refresh hints, button compress before send) |20| 3 | **Staging** | Guide eye through sequential animation; dim backgrounds to focus |21| 4 | **Straight Ahead & Pose to Pose** | Define key poses (start, end, maybe midpoint); let browser interpolate |22| 5 | **Follow Through & Overlapping** | Springs add organic overshoot-and-settle; too much stagger feels slow |23| 6 | **Slow In & Slow Out** | `ease-out` for entrances, `ease-in` for exits, `ease-in-out` for deliberate |24| 7 | **Arcs** | Curved paths feel natural; best for hero moments and playful interactions |25| 8 | **Secondary Action** | Flourishes supporting the main action (sparkles, sound, particles) |26| 9 | **Timing** | Keep interactions <300ms; be consistent across similar elements |27| 10 | **Exaggeration** | Push past accuracy sparingly (onboarding, empty states, confirmations) |28| 11 | **Solid Drawing** | Shadows suggest depth; CSS `perspective` gives real 3D |29| 12 | **Appeal** | The sum of all techniques applied with care and taste |3031### Key Guidelines3233- **Balance**: Too much animation turns professional software into a cartoon34- **Consistency**: Define timing scales early, reuse everywhere35- **Purpose**: Great animation is invisible — users think "this feels good"36- **Restraint**: Not everything needs to be animated3738---3940## Part B: Animation Performance Rules4142### Rendering Pipeline4344- **Composite** (cheapest): `transform`, `opacity`45- **Paint**: color, borders, gradients, masks, images, filters46- **Layout** (most expensive): size, position, flow, grid, flex4748### Rule Categories by Priority4950| Priority | Category | Impact |51|----------|----------|--------|52| 1 | Never Patterns | CRITICAL |53| 2 | Choose the Mechanism | CRITICAL |54| 3 | Measurement | HIGH |55| 4 | Scroll | HIGH |56| 5 | Paint | MEDIUM-HIGH |57| 6 | Layers | MEDIUM |58| 7 | Blur & Filters | MEDIUM |59| 8 | View Transitions | LOW |60| 9 | Tool Boundaries | CRITICAL |6162### 1. Never Patterns (CRITICAL)6364- Do not interleave layout reads and writes in the same frame65- Do not animate layout continuously on large surfaces66- Do not drive animation from `scrollTop`, `scrollY`, or scroll events67- No `requestAnimationFrame` loops without a stop condition68- Do not mix multiple animation systems that each measure or mutate layout6970### 2. Choose the Mechanism (CRITICAL)7172- Default to `transform` and `opacity` for motion73- JS-driven animation only when interaction requires it74- Paint/layout animation acceptable only on small, isolated surfaces75- One-shot effects acceptable more often than continuous motion76- Prefer downgrading technique over removing motion entirely7778### 3. Measurement (HIGH)7980- Measure once, then animate via transform or opacity81- Batch all DOM reads before writes82- Do not read layout repeatedly during an animation83- Prefer FLIP-style transitions for layout-like effects8485### 4. Scroll (HIGH)8687- Prefer Scroll/View Timelines for scroll-linked motion88- Use `IntersectionObserver` for visibility and pausing89- Do not poll scroll position for animation90- Pause or stop animations when off-screen9192### 5. Paint (MEDIUM-HIGH)9394- Paint-triggering animation allowed only on small, isolated elements95- Do not animate CSS variables for transform, opacity, or position96- Scope animated CSS variables locally; avoid inheritance9798### 6. Layers (MEDIUM)99100- Compositor motion requires layer promotion — never assume it101- Use `will-change` temporarily and surgically102- Avoid many or large promoted layers103104### 7. Blur & Filters (MEDIUM)105106- Keep blur ≤8px; use only for short, one-time effects107- Never animate blur continuously or on large surfaces108- Prefer opacity and translate before blur109110### 8. View Transitions (LOW)111112- Use only for navigation-level changes113- Avoid for interaction-heavy UI or when interruption is needed114115### 9. Tool Boundaries (CRITICAL)116117- Do not migrate animation libraries unless explicitly requested118- Apply rules within the existing animation system119120---121122## Part C: Interaction Design Patterns123124### Timing Guidelines125126| Duration | Use Case |127|----------|----------|128| 100-150ms | Micro-feedback (hovers, clicks) |129| 200-300ms | Small transitions (toggles, dropdowns) |130| 300-500ms | Medium transitions (modals, page changes) |131| 500ms+ | Complex choreographed animations |132133### Easing Functions134135```css136--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Entering */137--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Exiting */138--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Moving between */139--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Playful overshoot */140```141142### Loading States143144**Skeleton screens** (preserve layout):145146```tsx147function CardSkeleton() {148 return (149 <div className="animate-pulse">150 <div className="h-48 bg-gray-200 rounded-lg" />151 <div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />152 <div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />153 </div>154 );155}156```157158**Progress bar**:159160```tsx161function ProgressBar({ progress }: { progress: number }) {162 return (163 <div className="h-2 bg-gray-200 rounded-full overflow-hidden">164 <motion.div165 className="h-full bg-blue-600"166 initial={{ width: 0 }}167 animate={{ width: `${progress}%` }}168 transition={{ ease: "easeOut" }}169 />170 </div>171 );172}173```174175### State Transitions176177**Toggle with spring**:178179```tsx180function Toggle({ checked, onChange }) {181 return (182 <button183 role="switch" aria-checked={checked}184 onClick={() => onChange(!checked)}185 className={`relative w-12 h-6 rounded-full transition-colors duration-200186 ${checked ? "bg-blue-600" : "bg-gray-300"}`}187 >188 <motion.span189 className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"190 animate={{ x: checked ? 24 : 0 }}191 transition={{ type: "spring", stiffness: 500, damping: 30 }}192 />193 </button>194 );195}196```197198### Page Transitions199200```tsx201import { AnimatePresence, motion } from "framer-motion";202203function PageTransition({ children, key }) {204 return (205 <AnimatePresence mode="wait">206 <motion.div207 key={key}208 initial={{ opacity: 0, y: 20 }}209 animate={{ opacity: 1, y: 0 }}210 exit={{ opacity: 0, y: -20 }}211 transition={{ duration: 0.3 }}212 >213 {children}214 </motion.div>215 </AnimatePresence>216 );217}218```219220### Gesture Interactions221222**Swipe to dismiss**:223224```tsx225function SwipeCard({ children, onDismiss }) {226 return (227 <motion.div228 drag="x"229 dragConstraints={{ left: 0, right: 0 }}230 onDragEnd={(_, info) => {231 if (Math.abs(info.offset.x) > 100) onDismiss();232 }}233 className="cursor-grab active:cursor-grabbing"234 >235 {children}236 </motion.div>237 );238}239```240241### CSS Animation Patterns242243```css244@keyframes fadeIn {245 from { opacity: 0; transform: translateY(10px); }246 to { opacity: 1; transform: translateY(0); }247}248249.card {250 transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;251}252.card:hover {253 transform: translateY(-4px);254 box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);255}256```257258### Accessibility259260```css261@media (prefers-reduced-motion: reduce) {262 *, *::before, *::after {263 animation-duration: 0.01ms !important;264 animation-iteration-count: 1 !important;265 transition-duration: 0.01ms !important;266 }267}268```269270---271272## Checklist273274### Quality275- [ ] Motion has clear purpose (feedback, orientation, focus, continuity)276- [ ] Timing consistent across similar elements277- [ ] Springs used for organic movement where appropriate278- [ ] Animations feel right but aren't noticeable279280### Performance281- [ ] Using `transform` and `opacity` for motion (compositor-only)282- [ ] No interleaved DOM reads and writes283- [ ] No continuous layout animation on large surfaces284- [ ] Scroll-linked motion uses Scroll/View Timelines or IntersectionObserver285- [ ] `will-change` used temporarily, not permanently286- [ ] Blur ≤8px and only for one-shot effects287288### Accessibility289- [ ] `prefers-reduced-motion` respected290- [ ] Animations interruptible291- [ ] `transition: all` never used — properties listed explicitly292- [ ] No motion that blocks user interaction293294### Implementation295- [ ] Micro-feedback: 100-150ms296- [ ] Small transitions: 200-300ms297- [ ] Loading states use skeleton screens298- [ ] Hover states use transform/opacity (no layout shift)299300## References301302- [Framer Motion](https://www.framer.com/motion/)303- [CSS Animation Guide](https://web.dev/animations-guide/)304- [easing.dev](https://easing.dev)305- [The Illusion of Life: Disney Animation](https://www.amazon.com/Illusion-Life-Disney-Animation/dp/0786860707)306- [Layout-forcing properties](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)