🎬 animate — Motion & UI Animation
Build motion that would survive a strict design-engineering review, on the first pass. The skill is a router + build sequence: decide whether to animate, why, which tool, then write correct code with defensible easing and timing.
Three principles govern everything:
- The correct answer is sometimes zero animation. An action fired 100×/day (keyboard shortcut, command palette, toggle) should not animate at all. Saying "this shouldn't move" is success, not a dodge.
- Cheapest tool that works. Don't install a motion library for a fade; don't hand-roll a drawer that should be a component.
- Animate only
transform and opacity (plus sanctioned clip-path, and height for accordions). These skip layout and paint and run on the GPU.
- Use what's already there. If the project already depends on a motion library (Motion, GSAP, Anime.js, Lenis, Reanimated, Three.js, …), build on it before introducing a new dependency — a new library is justified only when the present one can't express the motion. Extend existing
--ease-* / --duration-* design tokens, never fork them.
Modes — quick commands
Every invocation resolves to one of five modes. Route on the verb, not the noun:
| Mode |
Trigger phrases |
Behavior |
Reference |
| build (default) |
"animate", "add motion", "make X feel alive", "build a transition" |
Run the 8-step build sequence and write the code |
this file |
| review |
"review the animation", "critique the motion in this diff" |
Flag-by-default diff review → findings table + Block/Approve verdict |
review-checklist.md |
| improve |
"improve the animations", "make this app feel better" |
Read-only recon → audit → prioritized plan (does not edit source) |
review-checklist.md |
| audit |
"audit the motion", "animation audit" |
Same as improve: severity-ordered findings + self-contained remediation plans |
review-checklist.md |
| find |
"what could animate here?", "make this more alive" |
Gate-driven hunt; rejects most candidates, proposes only high-leverage motion |
review-checklist.md |
In review / improve / audit / find, load references/review-checklist.md and follow its workflow — do not write animation code in those modes. Only build edits code.
When to Use
- Asked to animate / add motion / build a transition / make something feel alive.
- Implementing entrances, exits, micro-interactions, hover/press states, scroll reveal, page transitions, layout changes, drag/swipe gestures, loading states.
- Reviewing or auditing existing animation (use references/review-checklist.md).
- Choosing an animation library or tool for a feature.
Anti-Triggers
- Building a component (toast, drawer, dropdown, command menu) rather than an animation → that is a UI-library decision, not motion.
- Pure layout/visual design without motion →
refactor-ui.
- Extracting a design system / design tokens →
designscope.
- Native mobile animation is explicit-ask-only; do not reach for
references/mobile.md unless the user names a mobile platform.
Quick Reference
Library selection — cheapest that fits (walk down, stop at first match)
| Need |
Tool |
Load |
| Hover, press, color, class/attribute state toggle |
CSS transition |
inline (tables below) |
| Entry on mount, no JS state |
CSS @starting-style |
css-animations.md |
| Predetermined motion staying smooth while page is busy |
CSS animation (runs off main thread) |
css-animations.md |
| Programmatic control, no library |
WAAPI (element.animate()) |
css-animations.md |
| Springs, layout animations, exit animations, gestures |
Motion (motion.dev) — default for React |
motion-dev.md |
| Timeline choreography, scroll pinning, SVG morph, complex sequencing |
GSAP — explicit ask only |
gsap.md |
| Designer-authored vector/illustration, state machines |
Lottie / Rive |
other-libraries.md |
| Lightweight imperative (non-React, SVG, timeline) |
Anime.js v4 |
other-libraries.md |
| Real 3D (heavy — minimum unless asked) |
Three.js / R3F |
other-libraries.md |
| Native mobile (Reanimated / SwiftUI / Compose / Flutter) |
— explicit ask only |
mobile.md |
Recommendation: Motion for any React motion beyond a trivial state toggle; CSS for the quick fixes. Prefer WAAPI when you need JS control without a dependency. Reach for GSAP only when you genuinely need timeline sequencing or ScrollTrigger choreography that Motion/CSS can't express cleanly.
Heavy libraries (GSAP, Lenis, Three.js/WebGL, p5.js, Matter.js, Tone.js) load only when you're asked for them or the project already depends on them. WebGL is heavy and not widely supported — treat it as a last resort, never a default.
Easing — in decision order (never hand-roll a curve you don't know)
| Situation |
Easing |
| Entering |
ease-out |
| Exiting |
ease-in (accelerates away; reverse of the entrance) |
| Moving / morphing on screen |
ease-in-out |
| Hover / color change |
ease |
| Constant motion (marquee, progress) |
linear |
| Default |
ease-out |
Never ease-in on an entrance — it starts slow, delaying the exact moment the user watches. ease-in on an exit is correct: it accelerates the element away (the time-reverse of an ease-out entrance). Built-in easings are too weak; use strong tokens:
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* strong ease-out for UI */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* strong in-out for on-screen movement */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve */
Need another curve? Take it from easing.dev / easings.co — don't invent cubic-bezier(0.4, 0, 0.2, 1) from memory.
Duration — UI stays under 300ms
| Element |
Duration |
| Button press feedback |
100–160ms |
| Tooltips, small popovers |
125–200ms |
| Dropdowns, selects |
150–250ms |
| Modals, drawers |
200–500ms |
| Marketing / explanatory |
can be longer |
Reach for a spring for drag-with-momentum, "alive" elements, interruptible gestures, decorative mouse-tracking:
{ type: "spring", duration: 0.5, bounce: 0.2 } // Apple-style, easy to reason about
{ type: "spring", mass: 1, stiffness: 100, damping: 10 } // traditional physics, more control
Keep bounce in 0.1–0.3, and avoid it in most UI — reserve for drag-to-dismiss and playful interactions.
Reference map
| Reference |
When to load |
| principles.md |
Grounding the 12 principles, easing/physics, motion vocabulary |
| motion-dev.md |
Any Motion / Framer Motion implementation |
| css-animations.md |
CSS transitions/keyframes/@property/scroll-driven/WAAPI/View Transitions |
| performance.md |
Jank, 60fps, compositor-only properties, LCP/CLS |
| accessibility.md |
prefers-reduced-motion, vestibular safety |
| review-checklist.md |
Reviewing or auditing motion quality |
| gsap.md |
GSAP + ScrollTrigger (explicit ask only) |
| mobile.md |
Reanimated / SwiftUI / Compose / Flutter (explicit ask only) |
| other-libraries.md |
Anime.js, Lottie, Rive, SVG, Three.js |
Procedure
Run the sequence in order; step 0 resolves the stack, steps 1–2 gate everything.
Step 0: Detect the project's existing motion stack
Scan package.json (dependencies + devDependencies), import/require statements, lockfiles, Tailwind config, and existing --ease-* / --duration-* tokens. If a library is already present, use it first — propose a new dependency only when the present tool can't express the motion. Respect existing easing/duration tokens (extend, never fork).
Step 1: Should this animate at all?
| Frequency |
Decision |
| 100+×/day (keyboard shortcuts, command palette) |
No animation. Ever. Stop. |
| Tens of ×/day (hover, list navigation) |
Near-imperceptible, or nothing |
| Occasional (modals, drawers, toasts) |
Standard animation |
| Rare / first-time (onboarding, celebration) |
The delight budget lives here |
Keyboard-initiated actions are a disqualifier, not a judgment call. If the request fails this gate, say so plainly and offer the non-motion alternative (instant state change, static affordance).
Step 2: Name the purpose
One word: feedback · spatial consistency · state indication · preventing a jarring change · explanation (marketing only) · delight (only at the rare tier). Can't name it → don't build it. "It looks cool" on a frequently-seen element is a reason to stop. Data the user reads or acts on must not move for style.
Step 3: Pick the tool
Walk the selection table above; stop at the first that fits. If the task needs a component (toast/drawer/dropdown), use a UI library, not hand-rolled motion.
Step 4: Pick properties
Step 5: Easing and duration — or a spring
Apply the easing + duration tables above. No approximated values — every curve/duration comes from the tables or a known source.
Step 6: Interruption and exit
- Transitions, not keyframes, for anything triggered rapidly (toasts, toggles) — transitions retarget from current value; keyframes restart from zero.
- Springs for gestures (carry velocity through interruption).
- Exit the way it entered (a toast that slides up from the bottom leaves through the bottom).
- Asymmetric timing where the user decides: slow on the deliberate phase, snappy on the system response.
Step 7: Reduced motion + pointer gating ships with it
@media (prefers-reduced-motion: reduce) {
.el { animation: fade 0.2s ease; } /* keep opacity/color, drop transform motion */
}
@media (hover: hover) and (pointer: fine) {
.el:hover { transform: scale(1.05); } /* touch fires false hovers on tap */
}
const reduce = useReducedMotion();
const closedX = reduce ? 0 : '-100%';
Reduced motion = fewer and gentler animations, not zero. See accessibility.md.
Step 8: Write the code, then state the reasoning
Emit the code. Then in ≤3 lines: the gate result (frequency tier + named purpose), the ingredients (tool / properties / curve / duration, one line each), and what to feel-check that code can't settle (play at 2–5× duration, step frame-by-frame, test gestures on a real device, re-check next day).
Pitfalls
- Animating
width/height/margin/padding/top/left instead of transform/opacity — triggers layout + paint + composite.
transition: all — name the exact properties.
transform: scale(0) entrance — nothing appears from nothing; use scale(0.95) + opacity: 0.
ease-in on a UI element — starts slow, feels unresponsive.
- Built-in
ease-out on a deliberate animation — too weak; use cubic-bezier(0.23, 1, 0.32, 1).
- UI duration over 300ms without a reason.
- Keyframes on toasts/toggles — restart from zero; use transitions.
- Motion
x/y/scale shorthands under load — use the full transform string.
- Ungated
:hover motion — wrap in @media (hover: hover) and (pointer: fine).
- Missing
prefers-reduced-motion — ships with the animation, not as a follow-up.
- Everything entering at once — 30–80ms stagger.
- Driving a child transform via a CSS variable on the parent — recalcs styles for every child; set
transform directly.
- Reaching for GSAP or a mobile platform without an explicit ask — those references are gated; pulling them in burns tokens for the wrong problem.
Verification
- Run the gate: can you name the frequency tier and the purpose in one word each? If the answer was "no animation," stop there — that is the deliverable.
- Confirm only
transform/opacity (or sanctioned clip-path/height) are animated.
- Confirm the easing and duration come from the tables (not invented), and UI durations are < 300ms (or justified).
- Confirm
prefers-reduced-motion and (for hover) pointer gating are present.
- Confirm exit symmetry and interruption behavior (transitions or springs for rapidly-triggered elements).
- For a review/audit task, confirm references/review-checklist.md was applied and every finding cites a specific rule/value.
1---2name: animate3description: Design, build, review, and improve web UI animation and motion: library selection, easing/timing, and correct code for entrances, exits, micro-interactions, scroll, page transitions, hover/press states, and layout shifts. Use when asked to animate something, add motion, build a transition, make a component feel alive, review/audit/improve existing motion, or find places that should animate. Defaults to Motion (motion.dev, formerly Framer Motion) for React and CSS for quick fixes; uses any animation library already present in the project first; GSAP, WebGL, and native-mobile (Reanimated/SwiftUI/Compose/Flutter) are loaded only on explicit request.4license: MIT5---67# 🎬 animate — Motion & UI Animation89Build motion that would survive a strict design-engineering review, on the first pass. The skill is a **router + build sequence**: decide *whether* to animate, *why*, *which tool*, then write correct code with defensible easing and timing.1011Three principles govern everything:12131. **The correct answer is sometimes zero animation.** An action fired 100×/day (keyboard shortcut, command palette, toggle) should not animate at all. Saying "this shouldn't move" is success, not a dodge.142. **Cheapest tool that works.** Don't install a motion library for a fade; don't hand-roll a drawer that should be a component.153. **Animate only `transform` and `opacity`** (plus sanctioned `clip-path`, and `height` for accordions). These skip layout and paint and run on the GPU.164. **Use what's already there.** If the project already depends on a motion library (Motion, GSAP, Anime.js, Lenis, Reanimated, Three.js, …), build on it before introducing a new dependency — a new library is justified only when the present one can't express the motion. Extend existing `--ease-*` / `--duration-*` design tokens, never fork them.1718---1920## Modes — quick commands2122Every invocation resolves to one of five modes. Route on the *verb*, not the noun:2324| Mode | Trigger phrases | Behavior | Reference |25|:---|:---|:---|:---|26| **build** (default) | "animate", "add motion", "make X feel alive", "build a transition" | Run the 8-step build sequence and write the code | this file |27| **review** | "review the animation", "critique the motion in this diff" | Flag-by-default diff review → findings table + Block/Approve verdict | [review-checklist.md](references/review-checklist.md) |28| **improve** | "improve the animations", "make this app feel better" | Read-only recon → audit → prioritized plan (does **not** edit source) | [review-checklist.md](references/review-checklist.md) |29| **audit** | "audit the motion", "animation audit" | Same as **improve**: severity-ordered findings + self-contained remediation plans | [review-checklist.md](references/review-checklist.md) |30| **find** | "what could animate here?", "make this more alive" | Gate-driven hunt; rejects most candidates, proposes only high-leverage motion | [review-checklist.md](references/review-checklist.md) |3132In **review / improve / audit / find**, load `references/review-checklist.md` and follow its workflow — do not write animation code in those modes. Only **build** edits code.3334---3536## When to Use3738- Asked to **animate / add motion / build a transition / make something feel alive**.39- Implementing **entrances, exits, micro-interactions, hover/press states, scroll reveal, page transitions, layout changes, drag/swipe gestures, loading states**.40- **Reviewing or auditing** existing animation (use [references/review-checklist.md](references/review-checklist.md)).41- Choosing an **animation library or tool** for a feature.4243### Anti-Triggers4445- Building a *component* (toast, drawer, dropdown, command menu) rather than an animation → that is a UI-library decision, not motion.46- Pure **layout/visual design** without motion → [`refactor-ui`](../refactor-ui/SKILL.md).47- Extracting a **design system / design tokens** → [`designscope`](../designscope/SKILL.md).48- Native mobile animation is **explicit-ask-only**; do not reach for `references/mobile.md` unless the user names a mobile platform.4950---5152## Quick Reference5354### Library selection — cheapest that fits (walk down, stop at first match)5556| Need | Tool | Load |57|:---|:---|:---|58| Hover, press, color, class/attribute state toggle | **CSS transition** | inline (tables below) |59| Entry on mount, no JS state | **CSS `@starting-style`** | [css-animations.md](references/css-animations.md) |60| Predetermined motion staying smooth while page is busy | **CSS animation** (runs off main thread) | [css-animations.md](references/css-animations.md) |61| Programmatic control, no library | **WAAPI** (`element.animate()`) | [css-animations.md](references/css-animations.md) |62| Springs, layout animations, exit animations, gestures | **Motion** (`motion.dev`) — default for React | [motion-dev.md](references/motion-dev.md) |63| Timeline choreography, scroll pinning, SVG morph, complex sequencing | **GSAP** — *explicit ask only* | [gsap.md](references/gsap.md) |64| Designer-authored vector/illustration, state machines | **Lottie / Rive** | [other-libraries.md](references/other-libraries.md) |65| Lightweight imperative (non-React, SVG, timeline) | **Anime.js v4** | [other-libraries.md](references/other-libraries.md) |66| Real 3D (heavy — *minimum unless asked*) | **Three.js / R3F** | [other-libraries.md](references/other-libraries.md) |67| Native mobile (Reanimated / SwiftUI / Compose / Flutter) | — *explicit ask only* | [mobile.md](references/mobile.md) |6869**Recommendation:** **Motion** for any React motion beyond a trivial state toggle; **CSS** for the quick fixes. Prefer **WAAPI** when you need JS control without a dependency. Reach for **GSAP** only when you genuinely need timeline sequencing or ScrollTrigger choreography that Motion/CSS can't express cleanly.7071**Heavy libraries** (GSAP, Lenis, Three.js/WebGL, p5.js, Matter.js, Tone.js) load only when you're asked for them **or the project already depends on them**. WebGL is heavy and not widely supported — treat it as a last resort, never a default.7273### Easing — in decision order (never hand-roll a curve you don't know)7475| Situation | Easing |76|:---|:---|77| Entering | `ease-out` |78| Exiting | `ease-in` (accelerates away; reverse of the entrance) |79| Moving / morphing on screen | `ease-in-out` |80| Hover / color change | `ease` |81| Constant motion (marquee, progress) | `linear` |82| Default | `ease-out` |8384**Never `ease-in` on an entrance** — it starts slow, delaying the exact moment the user watches. `ease-in` on an *exit* is correct: it accelerates the element away (the time-reverse of an ease-out entrance). Built-in easings are too weak; use strong tokens:8586```css87--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* strong ease-out for UI */88--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* strong in-out for on-screen movement */89--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve */90```9192Need another curve? Take it from [easing.dev](https://easing.dev/) / [easings.co](https://easings.co/) — don't invent `cubic-bezier(0.4, 0, 0.2, 1)` from memory.9394### Duration — UI stays under 300ms9596| Element | Duration |97|:---|:---|98| Button press feedback | 100–160ms |99| Tooltips, small popovers | 125–200ms |100| Dropdowns, selects | 150–250ms |101| Modals, drawers | 200–500ms |102| Marketing / explanatory | can be longer |103104Reach for a **spring** for drag-with-momentum, "alive" elements, interruptible gestures, decorative mouse-tracking:105106```js107{ type: "spring", duration: 0.5, bounce: 0.2 } // Apple-style, easy to reason about108{ type: "spring", mass: 1, stiffness: 100, damping: 10 } // traditional physics, more control109```110111Keep bounce in 0.1–0.3, and avoid it in most UI — reserve for drag-to-dismiss and playful interactions.112113### Reference map114115| Reference | When to load |116|:---|:---|117| [principles.md](references/principles.md) | Grounding the 12 principles, easing/physics, motion vocabulary |118| [motion-dev.md](references/motion-dev.md) | Any Motion / Framer Motion implementation |119| [css-animations.md](references/css-animations.md) | CSS transitions/keyframes/@property/scroll-driven/WAAPI/View Transitions |120| [performance.md](references/performance.md) | Jank, 60fps, compositor-only properties, LCP/CLS |121| [accessibility.md](references/accessibility.md) | `prefers-reduced-motion`, vestibular safety |122| [review-checklist.md](references/review-checklist.md) | Reviewing or auditing motion quality |123| [gsap.md](references/gsap.md) | GSAP + ScrollTrigger (explicit ask only) |124| [mobile.md](references/mobile.md) | Reanimated / SwiftUI / Compose / Flutter (explicit ask only) |125| [other-libraries.md](references/other-libraries.md) | Anime.js, Lottie, Rive, SVG, Three.js |126127---128129## Procedure130131Run the sequence in order; step 0 resolves the stack, steps 1–2 gate everything.132133### Step 0: Detect the project's existing motion stack134135Scan `package.json` (`dependencies` + `devDependencies`), `import`/`require` statements, lockfiles, Tailwind config, and existing `--ease-*` / `--duration-*` tokens. If a library is already present, **use it first** — propose a new dependency only when the present tool can't express the motion. Respect existing easing/duration tokens (extend, never fork).136137### Step 1: Should this animate at all?138139| Frequency | Decision |140|:---|:---|141| 100+×/day (keyboard shortcuts, command palette) | **No animation. Ever.** Stop. |142| Tens of ×/day (hover, list navigation) | Near-imperceptible, or nothing |143| Occasional (modals, drawers, toasts) | Standard animation |144| Rare / first-time (onboarding, celebration) | The delight budget lives here |145146**Keyboard-initiated actions are a disqualifier, not a judgment call.** If the request fails this gate, say so plainly and offer the non-motion alternative (instant state change, static affordance).147148### Step 2: Name the purpose149150One word: **feedback** · **spatial consistency** · **state indication** · **preventing a jarring change** · **explanation** (marketing only) · **delight** (*only* at the rare tier). Can't name it → don't build it. "It looks cool" on a frequently-seen element is a reason to stop. Data the user reads or acts on must not move for style.151152### Step 3: Pick the tool153154Walk the selection table above; stop at the first that fits. If the task needs a *component* (toast/drawer/dropdown), use a UI library, not hand-rolled motion.155156### Step 4: Pick properties157158- **`transform` + `opacity` only** (`clip-path` sanctioned 4th; `height` tolerated for accordions).159- **Never `scale(0)`** — start from `scale(0.9–0.97)` + `opacity: 0`.160- **`transform-origin` at the trigger** for popovers/dropdowns/menus (`var(--transform-origin)`); **modals exempt** (stay centered).161- **Percentage `translate()`** is relative to element size — `translateY(100%)` = own height. Prefer over px.162- **Motion: use the full transform string.** `x`/`y`/`scale` shorthands are not hardware-accelerated and drop frames under load:163 ```jsx164 <motion.div animate={{ transform: "translateX(100px)" }} /> // accelerated165 <motion.div animate={{ x: 100 }} /> // drops frames under load166 ```167168### Step 5: Easing and duration — or a spring169170Apply the easing + duration tables above. **No approximated values** — every curve/duration comes from the tables or a known source.171172### Step 6: Interruption and exit173174- **Transitions, not keyframes,** for anything triggered rapidly (toasts, toggles) — transitions retarget from current value; keyframes restart from zero.175- **Springs for gestures** (carry velocity through interruption).176- **Exit the way it entered** (a toast that slides up from the bottom leaves through the bottom).177- **Asymmetric timing** where the user decides: slow on the deliberate phase, snappy on the system response.178179### Step 7: Reduced motion + pointer gating ships with it180181```css182@media (prefers-reduced-motion: reduce) {183 .el { animation: fade 0.2s ease; } /* keep opacity/color, drop transform motion */184}185@media (hover: hover) and (pointer: fine) {186 .el:hover { transform: scale(1.05); } /* touch fires false hovers on tap */187}188```189190```jsx191const reduce = useReducedMotion();192const closedX = reduce ? 0 : '-100%';193```194195Reduced motion = **fewer and gentler** animations, not zero. See [accessibility.md](references/accessibility.md).196197### Step 8: Write the code, then state the reasoning198199Emit the code. Then in ≤3 lines: the **gate result** (frequency tier + named purpose), the **ingredients** (tool / properties / curve / duration, one line each), and what to **feel-check** that code can't settle (play at 2–5× duration, step frame-by-frame, test gestures on a real device, re-check next day).200201---202203## Pitfalls204205- **Animating `width`/`height`/`margin`/`padding`/`top`/`left`** instead of `transform`/`opacity` — triggers layout + paint + composite.206- **`transition: all`** — name the exact properties.207- **`transform: scale(0)`** entrance — nothing appears from nothing; use `scale(0.95)` + `opacity: 0`.208- **`ease-in` on a UI element** — starts slow, feels unresponsive.209- **Built-in `ease-out` on a deliberate animation** — too weak; use `cubic-bezier(0.23, 1, 0.32, 1)`.210- **UI duration over 300ms** without a reason.211- **Keyframes on toasts/toggles** — restart from zero; use transitions.212- **Motion `x`/`y`/`scale` shorthands under load** — use the full `transform` string.213- **Ungated `:hover` motion** — wrap in `@media (hover: hover) and (pointer: fine)`.214- **Missing `prefers-reduced-motion`** — ships with the animation, not as a follow-up.215- **Everything entering at once** — 30–80ms stagger.216- **Driving a child transform via a CSS variable on the parent** — recalcs styles for every child; set `transform` directly.217- **Reaching for GSAP or a mobile platform without an explicit ask** — those references are gated; pulling them in burns tokens for the wrong problem.218219---220221## Verification222223- Run the gate: can you name the frequency tier and the purpose in one word each? If the answer was "no animation," stop there — that *is* the deliverable.224- Confirm only `transform`/`opacity` (or sanctioned `clip-path`/`height`) are animated.225- Confirm the easing and duration come from the tables (not invented), and UI durations are < 300ms (or justified).226- Confirm `prefers-reduced-motion` and (for hover) pointer gating are present.227- Confirm exit symmetry and interruption behavior (transitions or springs for rapidly-triggered elements).228- For a **review/audit** task, confirm [references/review-checklist.md](references/review-checklist.md) was applied and every finding cites a specific rule/value.