Route animation and motion work through the animations.dev toolkit. Use for animation design, implementation, review, performance, accessibility, Motion for React, CSS motion, prototyping, or motion briefs.
Scanned 8/31/2026
Install to Claude Code
npx -y skills add twikus/claude-configuration --skill animate --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Animate?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/twikus-animate)More formats (shields.io, HTML) on the badges page.
---
name: animate
description: Route animation and motion work through the animations.dev toolkit. Use for animation design, implementation, review, performance, accessibility, Motion for React, CSS motion, prototyping, or motion briefs.
metadata:
short-description: Route animation work to the right specialist
---
# Building Animations
The complete builder's guide to motion that feels right, distilled from Emil Kowalski's *Animations on the Web* course ([animations.dev](https://animations.dev/)). Use it to make decisions first, then implement.
## Modes and references
`animate` is the single animation skill. Its focused workflows and long-form lessons live as internal references under `references/`; they are not separate skills or commands.
Select the smallest relevant mode below. Read its linked `guide.md` completely before acting, then load only the adjacent recipes, standards, snippets, or templates that the guide explicitly requires. Resolve relative links from the guide's own directory.
| Route | Internal mode guide |
| --- | --- |
| CSS transitions, keyframes, transforms, hover, press, clip-path | [CSS animations](references/css-animations/guide.md) |
| Motion for React / Framer Motion | [Motion for React](references/motion-react/guide.md) |
| Strict review with approve/block verdict | [Review animations](references/review-animations/guide.md) |
| Read-only animation audit and implementation plan | [Improve animations](references/improve-animations/guide.md) |
| Find the few UI areas that should animate | [Find opportunities](references/find-animation-opportunities/guide.md) |
| Choose an animation or UI library | [Pick a library](references/pick-ui-library/guide.md) |
| Interview the user and produce a motion brief | [Motion brief](references/motion-brief/guide.md) |
| Name an animation effect | [Animation vocabulary](references/animation-vocabulary/guide.md) |
| Diagnose jank, frame budget, GPU, or rendering cost | [Animation performance](references/animation-performance/guide.md) |
| Reduced-motion and motion accessibility | [Animation accessibility](references/animation-accessibility/guide.md) |
| Build several animation/UI variants behind a picker | [Prototype variants](references/prototype/guide.md) |
If no focused mode is narrower than this core guide, use this file directly. If several apply, load them in dependency order: brief or library choice, implementation, then performance/accessibility/review.
## Initial Response
When this skill is first invoked without a specific question, respond only with:
> I'm ready to help you build animations that feel right, based on Emil Kowalski's animations.dev course. Tell me what you're animating.
Do not provide any other information until the user asks a question.
## Core Philosophy
An animation feels right when it satisfies three things at once:
1. **It feels natural.** It mirrors the physics of the real world. Nothing around us moves at a constant speed or appears out of nowhere — so `linear` easing feels lifeless and `scale(0)` entrances feel wrong.
2. **It has a purpose.** You can answer "why does this animate?" in one sentence, and neither you nor the user is surprised or annoyed by it.
3. **It's made with taste.** Taste is trained, not innate — the ability to tell good motion from bad and justify why. In a world where everyone's software works, taste is the differentiator.
Two consequences run through everything below:
- **Unseen details compound.** Most motion details users never consciously notice — that's the point. The aggregate of invisible correctness is what makes an interface feel expensive.
- **If everything animates, nothing stands out.** Motion is a spice, not the meal. Pace it through the experience; the more you add, the less each one is worth.
## Load These When Implementing
This file is the decision layer. When you move to code, load the companion reference for the exact recipe:
- **[css-techniques.md](css-techniques.md)** — transitions vs keyframes, transforms, `clip-path`, `@starting-style`, stagger, hover patterns, 3D.
- **[framer-motion.md](framer-motion.md)** — `motion/react`: `initial`/`animate`/`exit`, spring configs, `AnimatePresence` modes, `layout`/`layoutId`, motion values & hooks, animating height, and the component recipes (drawer, crossfade, morph, shared-element, trash).
- **[svg-animation.md](svg-animation.md)** — `viewBox`, line-drawing (`stroke-dashoffset`), `transform-box`/origin, path morphing, shakes, ambient "life."
Err on the side of loading a reference rather than approximating a value.
## The Animation Decision Framework
Answer these in order **before** writing animation code.
### 1. Should this animate at all?
Match motion to how often the user sees it:
| Frequency | Decision |
| --- | --- |
| 100+/day (keyboard shortcuts, command-palette toggle, arrow-key list nav) | **No animation. Ever.** |
| Tens/day (hover effects, list navigation) | Remove or drastically reduce |
| Occasional (modals, drawers, toasts) | Standard animation |
| Rare / first-time (onboarding, feedback, celebrations) | Can add delight |
**Never animate keyboard-initiated actions** — they repeat hundreds of times a day; animation makes them feel slow and disconnected. Raycast has no open/close animation — that's correct for something opened hundreds of times a day. A high-frequency selection highlight should be **instant** (track the cursor exactly), not a smooth fade that trails one step behind.
### 2. What's the purpose?
Every animation needs one of: **explanation** (marketing/onboarding), **feedback/responsiveness** (a button that reacts to a press), **spatial consistency** (an element enters and exits the same way), **state indication**, **preventing a jarring change** (a toast that eases in instead of popping), or — rarely — **delight** (reserved for interactions seen seldom, so it stays a pleasant surprise). "It looks cool" on a frequently-seen element is not a purpose.
### 3. Then pick the ingredients
Easing → duration → physicality → (spring?) → interruptibility → performance → accessibility. The rest of this file is those ingredients.
## Easing
Easing is the single most important part of an animation — it can make a bad animation look great or a great one feel wrong.
| Situation | Curve |
| --- | --- |
| Entering or exiting the screen | **`ease-out`** (fast start, gentle settle — feels responsive) |
| Moving / morphing while already on screen | **`ease-in-out`** (car accelerating then braking) |
| Hover / color / background / opacity | **`ease`** (asymmetric, elegant for small changes; CSS default) |
| Constant motion (marquee, spinner, timer, hold-to-delete) | **`linear`** |
| Default | **`ease-out`** |
**Never use `ease-in` on UI.** It starts slow — delaying the exact moment the user is watching — then accelerates into the stop, the opposite of how things settle.
**Built-in named curves are almost never strong enough.** Their acceleration is too weak, so animations feel flat. Every course example uses a **custom** curve. Prefer **asymmetric** curves (steep start, slow settle) — they feel alive and mimic a spring without one. When an animation feels flat, the curve is probably too weak, not the duration.
Curves worth reaching for:
```css
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); /* strong ease-out: hovers, reveals */
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); /* button press */
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); /* on-screen back-and-forth */
--ease-vaul: cubic-bezier(0.32, 0.72, 0, 1); /* iOS sheet — extremely steep start */
--ease-drawer-height: cubic-bezier(0.25, 1, 0.5, 1); /* snappy height change */
```
**Pair entering and exiting elements to the same direction and curve family** so the interaction reads as one coherent space.
## Duration
Keep UI animations **under ~300ms** unless justified. A 180ms dropdown feels more responsive than a 400ms one. Faster spinners make loads *feel* faster.
| Element | Duration |
| --- | --- |
| Button press | ~150ms |
| Tooltips, small popovers | 125–200ms |
| Dropdowns, selects | 150–250ms |
| Modals, drawers | 200–500ms |
| Small floating drawer | ~270ms |
| Wide menu resize | ~250ms |
| Big element crossing the screen | up to ~1s |
**Duration and easing are inseparable.** A steep curve can afford a longer duration (Vaul's 500ms doesn't feel slow because the curve front-loads the movement); a weak curve must be shorter. **Choose the easing first, then tune duration to it.** Duration scales with **element size and travel distance** (a bigger element is heavier). **Exits are shorter and simpler than entries** — the user already decided; get out of the way. Too fast is as bad as too slow. Marketing pages can run longer; product must feel fast.
For transitions whose size varies (an auto-height drawer), make duration **proportional to how much changed** so small changes don't over-animate — see the adaptive-duration recipe in [framer-motion.md](framer-motion.md).
## Physicality
- **Never animate from `scale(0)`.** Start entrances from `scale(0.9–0.95)` + `opacity: 0`. Nothing appears from nothing; a near-full start reads as "it was always almost there." Bigger floating elements start closer to 1 (a nav menu uses `scale(0.98)`).
- **Button press:** `transform: scale(0.97)` on `:active`, `transition: transform ~150ms`. Press feedback is *felt, not seen* — `scale(0.9)` visibly collapses. Buttons feel best with **both** hover and press feedback; hover with nothing on click feels dead.
- **Hover scale:** 1–2% is plenty (`scale(1.02)`). `hover:scale-105` inflates like a balloon. Hover duration 100–150ms.
- **Origin-aware popovers.** Scale from the **trigger**, not the center (the CSS default `transform-origin: center` is wrong for almost every triggered element). Use the library's variable:
```css
.popover { transform-origin: var(--radix-popover-content-transform-origin); } /* Radix */
.popover { transform-origin: var(--transform-origin); } /* Base UI */
.menu { transform-origin: top center; } /* trigger above */
```
**Modals are exempt** — they appear centered, keep `transform-origin: center`.
- **Never put a hover lift on the hover target itself.** Animating `translateY` on the hovered element moves it out from under the cursor → hover ends → it drops → flicker loop. Move the lift to an inner child; the parent stays under the cursor.
## Springs
Springs simulate physics (mass, stiffness/tension, damping) with no fixed duration, so they feel organic and alive. Reach for them for: drag with momentum, "alive" elements (Dynamic Island), interruptible gestures, and cursor-following. Simple color/opacity changes don't need a spring. Real springs are impossible in pure CSS (only approximable with `linear()`).
```js
// Apple / Motion style (easier to reason about) — great for UI text/state swaps
{ type: "spring", duration: 0.3, bounce: 0 }
{ type: "spring", duration: 0.5, bounce: 0.2 }
// Physics form (more control)
{ type: "spring", stiffness: 100, damping: 10, mass: 0.75 }
```
- **Default bounce to 0.** No overshoot keeps UI natural and elegant. Add bounce only intentionally — a slight bounce at the *end of a drag* (a drag applies force); a press-to-close gets none. Bounce is personality: more = playful, zero = serious.
- **Bounce scales inversely with element size** — smaller elements need *more* bounce to read the same amount.
- **Springs are interruptible** — redirected mid-motion, they carry velocity, so gestures the user reverses stay smooth. This is why the Sonner toast bug (new toast jumping) was a keyframe/interruptibility problem.
- **"Weird" spring motion is usually fixed by increasing `damping`.**
## Interruptibility
Anything triggered rapidly (toasts, toggles, drawers, accordions, drags) must animate **from its current state**, not restart. CSS **transitions** and **springs** are interruptible; `@keyframes` restart from zero and make new items jump. Prefer transitions/springs for dynamic UI, and `@keyframes` only for autonomous, looping, or one-shot motion. Use `@starting-style` to animate an enter without JS — see [css-techniques.md](css-techniques.md).
## Performance
**The golden rule: only animate `transform` and `opacity`.** They run on the GPU (Composite step only). `width`/`height`/`margin`/`padding`/`top`/`left` trigger Layout + Paint + Composite and drop frames. Prefer `transform: translate` (percentages — relative to the element's own size) over `margin`/`top`, and `scale` over animating dimensions. Target 60fps (≤16.7ms/frame).
- **Framer Motion `x`/`y`/`scale` shorthands are not hardware-accelerated** — they run on the main thread via rAF. When motion must stay smooth under load, animate the full `transform` string.
- **Don't drive child transforms through a CSS variable on a shared parent** — inherited vars recalc styles for all descendants (the Vaul lag past ~20 items). Set `transform` directly on the element.
- **CSS/WAAPI beat JS under load** — they run off the main thread. Use CSS for predetermined motion, JS for dynamic/interruptible.
- **`will-change: transform`** fixes 1px GPU/CPU shift and promotes heavy/filtered elements to their own layer — but add it (and `contain: layout style paint`, `translateZ(0)`) **only once you see dropped frames**; too many layers cost memory.
- **Keep animated `blur()` ≤ ~20px** — blur gets laggy fast, especially in Safari.
For the full frame-budget model (the Layout/Paint/Composite pipeline, main-thread vs GPU, React re-renders, and a diagnosis checklist for dropped frames), load the [animation performance guide](references/animation-performance/guide.md).
## Stagger, hierarchy & orchestration
Stagger group entrances 30–80ms apart — longer feels slow, and stagger is decorative so it must never block interaction. **Vary the delay and distance by visual importance**: the most important element appears first with the most screen time; the least important can just fade in without sliding. **Uniform stagger** (identical delay/distance/easing per item) kills hierarchy and feels artificial.
**One entrance per container.** Don't slide a panel in *and* trickle its children in — slide it in with content already there. Sometimes the best animation is no animation.
## Cohesion & spatial consistency
- All of a component's sub-animations should share a timing feel so it reads as a **single entity** (the Family Drawer overrides Vaul's 500ms to 200ms so opening and height changes feel unified).
- **Exit direction matches entry direction**; navigation maps **forward = left, back = right**. A zoomed view expands from its thumbnail (object permanence) — it doesn't fade in from nowhere.
- Match motion to the component's **personality** — playful can be bouncier; a dashboard stays crisp (Sonner uses `ease`, slightly slower, to feel elegant).
- Prefer a **crossfade with a subtle directional hint** (8px shift + opacity + light blur) over a heavy full slide for small, structurally-similar content.
- When a crossfade shows two overlapping states despite tuning, add a subtle **`filter: blur(2px)`** during the transition to blend them into one perceived transformation.
## Accessibility
**Reduced motion means gentler, not zero.** Keep transitions that aid comprehension (opacity/color); remove movement and position changes. For purely decorative motion, disable it entirely (a lingering float would falsely imply interactivity).
```css
@media (prefers-reduced-motion: reduce) {
.element { animation: fade 0.2s ease; } /* replace movement, don't just delete */
}
@media (hover: hover) and (pointer: fine) {
.element:hover { transform: scale(1.02); } /* gate hover — touch fires false hovers on tap */
}
```
In Framer Motion, `useReducedMotion()` branches values, and `<MotionConfig reducedMotion="user">` animates only opacity/background app-wide. Make tap targets **≥ 44×44px** (enlarge with an invisible `::before` hitbox). On touch, hover+click fire together — detect `pointer: coarse` and use a two-tap pattern (first tap = hover, second = click).
For the two-variant workflow and the recipes for autoplaying media, looping animation, and smooth scrolling, load the [animation accessibility guide](references/animation-accessibility/guide.md).
## Process
Great animations take iteration, not one sitting.
- **Record and scrub** the reference (and your own work) frame by frame; tune magic transform values live in the console.
- **Don't code and ship in one sitting** — review with fresh eyes the next day (Sonner's transitions were replayed and tweaked daily for days).
- **Test gestures on real devices** (hit the dev server by IP; opacity-heavy motion wants high-refresh screens).
- Steal like an artist: recreate great animations by studying proven products rather than inventing patterns.
## Review Format (when asked to review)
If asked to review animation code, output a single markdown table — never a "Before:/After:" list.
| Before | After | Why |
| --- | --- | --- |
| `transform: scale(0)` | `transform: scale(0.95); opacity: 0` | Nothing appears from nothing |
| `ease-in` on dropdown | strong `ease-out` custom curve | `ease-in` delays the moment the user watches most |
| `hover:scale-105` | `hover:scale-[1.02]`, gated behind `(hover: hover)` | 5% inflates; 1–2% is enough, and touch shouldn't trigger it |
| `transform-origin: center` on popover | `var(--radix-popover-content-transform-origin)` | Popovers scale from their trigger (modals stay centered) |
For a dedicated, exhaustive reviewer with a strict block/approve verdict, load the [review mode](references/review-animations/guide.md). To name an effect, load the [vocabulary mode](references/animation-vocabulary/guide.md).
No comments yet. Be the first to comment!