# Motion Cookbook
The single source of truth for motion **recipes** — implementation patterns and code. In Create mode this is your primary reference; in Audit mode load it when making implementation recommendations. Designer philosophy and decision frameworks live in the per-designer reference files; the code lives here.
---
## 1. Enter & Exit Animations
### Enter Animation Recipe (Jakub)
A standard enter animation combines three properties:
- **Opacity**: 0 → 1
- **TranslateY**: ~8px → 0 (or calc(-100% - 4px) for full container slides)
- **Blur**: 4px → 0px
```jsx
initial={{ opacity: 0, translateY: "calc(-100% - 4px)", filter: "blur(4px)" }}
animate={{ opacity: 1, translateY: 0, filter: "blur(0px)" }}
transition={{ type: "spring", duration: 0.45, bounce: 0 }}
```
**Why blur?** It creates a "materializing" effect that feels more physical than opacity alone. The element appears to come into focus, not just fade in.
### Exit Animation Subtlety (Jakub)
**Key Insight**: Exit animations should be subtler than enter animations.
When a component exits, it doesn't need the same amount of movement or attention as when entering. The user's focus is moving to what comes next, not what's leaving.
```jsx
// Instead of full exit movement:
exit={{ translateY: "calc(-100% - 4px)" }}
// Use a subtle fixed value:
exit={{ translateY: "-12px", opacity: 0, filter: "blur(4px)" }}
```
**Why this works**: Exits become softer, less jarring, and don't compete for attention with whatever is entering or remaining.
**When NOT to use subtle exits**:
- When the exit itself is meaningful (user-initiated dismissal)
- When you need to emphasize something leaving (error clearing, item deletion)
- Full-page transitions where directional continuity matters
### Fill Mode for Persistence (Jhey)
Use `animation-fill-mode` to prevent jarring visual resets:
- `forwards`: Element retains animation styling after completion
- `backwards`: Element retains style from first keyframe before animation starts
- `both`: Retains styling in both directions
**Critical for**: Fade-in sequences with delays. Without `backwards`, elements flash at full opacity before their delayed animation starts, then pop to invisible, then fade in.
---
## 2. Easing & Timing
### Duration Impacts Naturalness
> "Duration is all about timing, and timing has a big impact on the movement's naturalness." — Jhey Tompkins
### Custom Easing is Essential (Emil)
> "Easing is the most important part of any animation. It can make a bad animation feel great."
Built-in CSS easing (`ease`, `ease-in-out`) lacks strength. Always use custom Bézier curves for professional results. Resources: easing.dev, easings.co
### Easing Selection Guidelines (Jhey)
Each easing curve communicates something to the viewer. **Context matters more than rules.**
| Easing | Feel | Good For |
|--------|------|----------|
| `ease-out` | Fast start, gentle stop | Elements entering view (arriving) |
| `ease-in` | Gentle start, fast exit | Elements leaving view (departing) |
| `ease-in-out` | Gentle both ends | Elements changing state while visible |
| `linear` | Constant speed | Continuous loops, progress indicators |
| `spring` | Natural deceleration | Interactive elements, professional UI |
**The Context Rule**:
> "You wouldn't use 'Elastic' for a bank's website, but it might work perfectly for an energetic site for children."
Brand personality should drive easing choices. A playful brand can use bouncy, elastic easing. A professional brand should use subtle springs or ease-out.
**When NOT to use bouncy/elastic easing**:
- Professional/enterprise applications
- Frequently repeated interactions (gets tiresome)
- Error states or serious UI
- When users need to complete tasks quickly
### Spring Animations (Jakub)
Prefer spring animations over linear/ease for more natural-feeling motion:
```jsx
transition={{ type: "spring", duration: 0.45, bounce: 0 }}
transition={{ type: "spring", duration: 0.55, bounce: 0.1 }}
```
**Why `bounce: 0`?** It gives smooth deceleration without overshoot—professional and refined. Reserve bounce > 0 for playful contexts.
### The linear() Function (Jhey)
CSS `linear()` enables bounce, elastic, and spring effects in pure CSS:
```css
:root {
--bounce-easing: linear(
0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
0.973, 1, 0.988, 0.984, 0.988, 1
);
}
```
Use Jake Archibald's linear() generator for custom curves: https://linear-easing-generator.netlify.app/
### Stagger Techniques (Jhey)
`animation-delay` only applies once (not per iteration). Approaches:
1. **Different delays with finite iterations** — Works for one-time sequences
2. **Pad keyframes** to create stagger within the animation:
```css
@keyframes spin {
0%, 50% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
```
3. **Negative delays** for "already in progress" effects:
```css
.element { animation-delay: calc(var(--index) * -0.2s); }
```
This makes animations appear mid-flight from the start—useful for staggered continuous animations.
---
## 3. Visual Effects
### Shadows Instead of Borders (Jakub)
In light mode, prefer subtle multi-layer box-shadows over solid borders:
```css
.card {
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
0px 1px 2px -1px rgba(0, 0, 0, 0.06),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
}
/* Slightly darker on hover */
.card:hover {
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.08),
0px 1px 2px -1px rgba(0, 0, 0, 0.08),
0px 2px 4px 0px rgba(0, 0, 0, 0.06);
}
```
**Why shadows over borders?**
- Shadows adapt to any background (images, gradients, varied colors) because they use transparency
- Borders are solid colors that may clash with dynamic backgrounds
- Multi-layer shadows create depth; single borders feel flat
- Shadows can be transitioned smoothly with `transition: box-shadow`
**When borders are fine**:
- Dark mode (shadows less visible anyway)
- When you need hard edges intentionally
- Simple interfaces where depth isn't needed
### Gradients & Color Spaces (Jakub)
- Use `oklch` for gradients to avoid muddy midpoints:
```css
element { background: linear-gradient(in oklch, blue, red); }
```
- **Color hints** control where the blend midpoint appears (different from color stops)
- Layer gradients with `background-blend-mode` for unique effects
**Why oklch?** It interpolates through perceptually uniform color space, avoiding the gray/muddy zone that sRGB hits when blending complementary colors.
### Blur as a Signal (Jakub)
Blur (via `filter: blur()`) combined with opacity and translate creates a "materializing" effect. Use blur to signal:
- **Entering focus**: blur → sharp
- **Losing relevance**: sharp → blur
- **State transitions**: blur during, sharp after
---
## 4. Optical Alignment
### Geometric vs. Optical (Jakub)
> "Sometimes it's necessary to break out of geometric alignment to make things feel visually balanced."
**Buttons with icons**: Reduce padding on the icon side so content appears centered:
```
[ Icon Text ] ← Geometric (mathematically centered, feels off)
[ Icon Text ] ← Optical (visually centered, feels right)
```
**Play button icons**: The triangle points right, creating visual weight on the left. Shift it slightly right to appear centered.
**Icons in general**: Many icon packs account for optical balance, but asymmetric shapes (arrows, play, chevrons) may need manual margin/padding adjustment.
**The rule**: If it looks wrong despite being mathematically correct, trust your eyes and adjust.
---
## 5. Icon & State Animations (Jakub)
### Contextual Icon Transitions
When icons change contextually (copy → check, loading → done), animate:
- Opacity
- Scale
- Blur
```jsx
{isCopied ? (
) : (
)}
```
**Why animate icon swaps?** Instant swaps feel jarring and can be missed. Animated transitions:
- Draw attention to the state change
- Feel responsive and polished
- Give the user confidence their action registered
---
## 6. Shared Layout Animations (Jakub)
### FLIP Technique via layoutId
Motion's `layoutId` prop enables smooth transitions between completely different components:
```jsx
// In one location:
// In another location:
```
Motion automatically animates between them using the FLIP technique (First, Last, Inverse, Play).
### Best Practices
- Keep elements with `layoutId` **outside** of `AnimatePresence` to avoid conflicts
- If inside `AnimatePresence`, the initial/exit animations will trigger during layout animation (looks bad with opacity)
- Multiple elements can animate if each has a unique `layoutId`
- Works for different heights, widths, positions, and even component types (card → modal)
---
## 7. CSS Custom Properties & @property (Jhey)
### Type Specification Unlocks Animation
The `@property` rule lets you declare types for CSS variables, enabling smooth interpolation:
```css
@property --hue {
initial-value: 0;
inherits: false;
syntax: '';
}
@keyframes rainbow {
to { --hue: 360; }
}
```
**Available types**: length, number, percentage, color, angle, time, integer, transform-list
**Why this matters**: Without `@property`, CSS sees custom properties as strings. Strings can't interpolate—they just swap. With a declared type, the browser knows how to smoothly transition between values.
### Decompose Complex Transforms
Instead of animating a monolithic transform (which can't interpolate curved paths), split into typed properties:
```css
@property --x { syntax: ''; initial-value: 0%; inherits: false; }
@property --y { syntax: ''; initial-value: 0%; inherits: false; }
.ball {
transform: translateX(var(--x)) translateY(var(--y));
animation: throw 1s;
}
@keyframes throw {
0% { --x: -500%; }
50% { --y: -250%; }
100% { --x: 500%; }
}
```
This creates curved motion paths that would be impossible with standard transform animation—the ball arcs through space rather than moving in straight lines.
### Scoped Variables for Dynamic Behavior (Jhey)
CSS custom properties respect scope, enabling powerful patterns:
```css
.item { --delay: 0; animation-delay: calc(var(--delay) * 100ms); }
.item:nth-child(1) { --delay: 0; }
.item:nth-child(2) { --delay: 1; }
.item:nth-child(3) { --delay: 2; }
```
Use scoped variables to create varied behavior from a single animation definition.
---
## 8. 3D CSS (Jhey)
### Think in Cuboids
> "Think in cubes instead of boxes" — Jhey Tompkins
Complex 3D scenes are assemblies of cube-shaped elements (like LEGO). Decompose any 3D object into cuboids.
### Essential Setup
```css
.scene {
transform-style: preserve-3d;
perspective: 1000px;
}
```
### Responsive 3D
Use CSS variables for dimensions and `vmin` units:
```css
.cube {
--size: 10vmin;
width: var(--size);
height: var(--size);
}
```
---
## 9. Clip-Path Animations (Emil)
### Why clip-path?
- Hardware-accelerated rendering
- No layout shifts
- No additional DOM elements needed
- Smoother than width/height animations
### Basic Syntax
```css
clip-path: inset(top right bottom left);
clip-path: circle(radius at x y);
clip-path: polygon(coordinates);
```
### Image Reveal Effect
```css
.reveal {
clip-path: inset(0 0 100% 0); /* Hidden */
animation: reveal 1s forwards cubic-bezier(0.77, 0, 0.175, 1);
}
@keyframes reveal {
to { clip-path: inset(0 0 0 0); } /* Fully visible */
}
```
### Tab Transitions
Duplicate tab lists with different styling. Animate the overlay's clip-path to reveal only the active tab—creates smooth color transitions without timing issues.
### Scroll-Driven with clip-path
```javascript
const clipPathY = useTransform(scrollYProgress, [0, 1], ["100%", "0%"]);
const motionClipPath = useMotionTemplate`inset(0 0 ${clipPathY} 0)`;
```
### Text Mask Effect
Stack elements with complementary clip-paths:
```css
.top { clip-path: inset(0 0 50% 0); } /* Shows top half */
.bottom { clip-path: inset(50% 0 0 0); } /* Shows bottom half */
```
Adjust values on mouse interaction for seamless transitions.
---
## 10. Button & Interactive Feedback (Emil)
### Scale on Press
Add immediate tactile feedback:
```css
button:active {
transform: scale(0.97);
}
```
### Don't Animate from scale(0)
```jsx
// BAD: Unnatural motion
initial={{ scale: 0 }}
// GOOD: Natural, gentle motion
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
```
### Tooltip Delay Pattern
First tooltip in a group: delay + animation. Subsequent tooltips: instant.
```css
[data-instant] {
transition-duration: 0ms;
}
```
### Blur as a Bridge
When state transitions aren't smooth enough, add blur to mask imperfections:
```css
.transitioning {
filter: blur(2px);
}
```
---
## 11. CSS Transitions vs Keyframes (Emil)
### Interruptibility Problem
CSS keyframes can't be interrupted mid-animation. When users rapidly trigger actions, elements "jump" to new positions rather than smoothly retargeting.
**Solution**: Use CSS transitions with state-driven classes:
```jsx
useEffect(() => {
setMounted(true);
}, []);
```
```css
.element {
transform: translateY(100%);
transition: transform 400ms ease;
}
.element.mounted {
transform: translateY(0);
}
```
### Direct Style Updates for Performance
CSS variables cause style recalculation across all children. For frequent updates (drag operations), update styles directly:
```javascript
// BAD: CSS variable (expensive cascade)
element.style.setProperty('--drag-y', `${y}px`);
// GOOD: Direct style (no cascade)
element.style.transform = `translateY(${y}px)`;
```
### Momentum-Based Dismissal
Use velocity (distance / time) instead of distance thresholds:
```javascript
const velocity = dragDistance / elapsedTime;
if (velocity > 0.11) dismiss();
```
Fast, short gestures should work—users shouldn't need to drag far.
### Damping for Natural Boundaries
When dragging past boundaries, reduce movement progressively. Things in real life slow down before stopping.
---
## 12. Spring Physics (Emil)
### Key Parameters
| Parameter | Effect |
|-----------|--------|
| **Stiffness** | How quickly spring reaches target (higher = faster) |
| **Damping** | How quickly oscillations settle (higher = less bounce) |
| **Mass** | Weight of object (higher = more momentum) |
### Spring for Mouse Position
```javascript
const springConfig = { stiffness: 300, damping: 30 };
const x = useSpring(mouseX, springConfig);
const y = useSpring(mouseY, springConfig);
```
Use `useSpring` for any value that should interpolate smoothly rather than snap—nothing in the real world changes instantly.
### Interruptibility
Great animations can be interrupted mid-play:
- Framer Motion supports interruption natively
- CSS transitions allow smooth interruption before completion
- Test by clicking rapidly—animations should blend, not queue
---
## 13. Origin-Aware Animations (Emil)
Animations should originate from their logical source:
```css
/* Dropdown from button should expand from button, not center */
.dropdown {
transform-origin: top center;
}
```
**Component library support:**
- Base UI: `--transform-origin` CSS variable
- Radix UI: `--radix-dropdown-menu-content-transform-origin`
---
## 14. Scroll-Driven Animations (Jhey)
### The Core Problem
Scroll-driven animations are tied to scroll **speed**. If users scroll slowly, animations play slowly. This feels wrong for most UI—you want animations to trigger at a scroll position, not be controlled by scroll speed.
### Duration Control Pattern
Use two coordinated animations:
1. **Trigger animation**: Scroll-driven, toggles a custom property when element enters view
2. **Main animation**: Traditional duration-based, activated via Style Query
This severs the connection between scroll speed and animation timing—the animation runs over a fixed duration once triggered, regardless of how fast the user scrolled.
### Progressive Enhancement
Always provide fallbacks:
```javascript
// IntersectionObserver fallback for browsers without scroll-driven animation support
if (!CSS.supports('animation-timeline', 'scroll()')) {
// Use IntersectionObserver instead
}
```