ab2308441c
The repository had two skill directories. `skills/` held the four written for this project; `.agents/skills/` held the ones the agent context refers to. Nothing said which an agent should read, and `.agents/ORCHESTRATOR.md` only ever pointed at the second. Move the first four into `.agents/skills/` so there is one location, and add the vendored packages this chapter work used: `animation-vocabulary` and `improve-animations` (emilkowalski/skills), `teach` (mattpocock/skills), plus a local `translation` skill and `audit-translations.mjs` for the EN/PT pairs. `skills-lock.json` pins the vendored three by source and content hash, so a later re-vendor is a diff rather than a guess. `.claude/skills/` is symlinks into `.agents/skills/`, which is what makes them loadable here without a second copy on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
7.6 KiB
Markdown
180 lines
7.6 KiB
Markdown
# Animation Audit Playbook
|
||
|
||
The eight audit categories, what to look for in each, and the exact target
|
||
values to cite in findings and plans. Distilled from Emil Kowalski's design
|
||
engineering philosophy ([emilkowal.ski](https://emilkowal.ski/)). Never
|
||
approximate a value that appears here — copy it.
|
||
|
||
## 1. Purpose & frequency
|
||
|
||
Every animation must answer "why does this animate?" — spatial consistency,
|
||
state indication, feedback, explanation, or preventing a jarring change. "It
|
||
looks cool" on a frequently-seen element is not a purpose.
|
||
|
||
| Frequency | Decision |
|
||
| ----------------------------------------------------------- | ---------------------------- |
|
||
| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever. |
|
||
| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce |
|
||
| Occasional (modals, drawers, toasts) | Standard animation |
|
||
| Rare / first-time (onboarding, feedback, celebrations) | Can add delight |
|
||
|
||
Hunt for: animations on keyboard-initiated actions, command palettes with
|
||
open/close transitions (Raycast has none — correct), decorative motion on list
|
||
items or hover states hit constantly. The strongest fix is often **delete the
|
||
animation**.
|
||
|
||
## 2. Easing & duration
|
||
|
||
Decision order for easing:
|
||
|
||
- Entering or exiting → **`ease-out`** (starts fast, feels responsive)
|
||
- Moving / morphing on screen → **`ease-in-out`**
|
||
- Hover / color change → **`ease`**
|
||
- Constant motion (marquee, progress) → **`linear`**
|
||
- Default → **`ease-out`**
|
||
|
||
**`ease-in` on UI is always a finding** — it starts slow, delaying the exact
|
||
moment the user is watching. Built-in CSS easings are too weak for deliberate
|
||
motion; plans should introduce strong custom curves (as tokens, matching repo
|
||
conventions):
|
||
|
||
```css
|
||
--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 ease-in-out for on-screen movement */
|
||
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS-like drawer curve */
|
||
```
|
||
|
||
Duration budgets — **UI animations stay 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 |
|
||
|
||
Hunt for: `ease-in` anywhere, bare `ease`/`linear` on entrances, durations >
|
||
300ms on UI elements, tooltip delay + animation on every tooltip in a toolbar
|
||
(after the first, they should be instant).
|
||
|
||
## 3. Physicality & origin
|
||
|
||
- **Never `scale(0)`** — nothing in the real world appears from nothing. Target:
|
||
`scale(0.9–0.97)` + `opacity: 0`.
|
||
- **Popovers/dropdowns/tooltips scale from their trigger**, not center:
|
||
```css
|
||
.popover {
|
||
transform-origin: var(--transform-origin);
|
||
} /* Base UI */
|
||
```
|
||
**Modals are exempt** — they appear centered; `transform-origin: center` is
|
||
correct there. Do not report it.
|
||
- **Press feedback**: `transform: scale(0.97)` on `:active` with
|
||
`transition: transform 160ms ease-out`. Keep it subtle (0.95–0.98).
|
||
|
||
Hunt for: `scale(0)`, pure-fade entrances with no initial transform,
|
||
`transform-origin: center` (or none) on trigger-anchored elements, pressable
|
||
elements with no press feedback.
|
||
|
||
## 4. Interruptibility
|
||
|
||
CSS **transitions** retarget from the current state mid-animation; **keyframes**
|
||
restart from zero. Anything triggered rapidly or reversible mid-motion (toasts
|
||
stacking, toggles, drags, expand/collapse) must use transitions or springs.
|
||
|
||
- Entry without JS: `@starting-style` (legacy fallback: a `data-mounted`
|
||
attribute set in `useEffect`).
|
||
- Gesture-driven motion should use springs — they carry velocity when
|
||
interrupted.
|
||
- Spring configs, Apple-style (recommended):
|
||
`{ type: "spring", duration: 0.5, bounce: 0.2 }`. Keep bounce subtle
|
||
(0.1–0.3); reserve visible bounce for drag-to-dismiss and playful moments.
|
||
- **Asymmetric timing**: deliberate phases (press, hold, destructive confirm)
|
||
animate slower; the system's response snaps. Symmetric timing on
|
||
press-and-release is a finding.
|
||
|
||
Hunt for: `@keyframes` on toasts/toggles/rapidly-triggered UI, gesture handlers
|
||
that tween with fixed-duration keyframes, drags without velocity-based dismissal
|
||
(dismiss on `Math.abs(distance)/elapsedMs > ~0.11`, not distance thresholds
|
||
alone), hard stops at drag boundaries instead of rising friction.
|
||
|
||
## 5. Performance
|
||
|
||
- **Animate `transform` and `opacity` only.**
|
||
`width`/`height`/`margin`/`padding`/`top`/`left` trigger layout + paint +
|
||
composite.
|
||
- **`transition: all`** animates unintended properties off-GPU — always a
|
||
finding.
|
||
- **Framer Motion `x`/`y`/`scale` shorthands are not hardware-accelerated** —
|
||
they run on the main thread and drop frames under load. Target: the full
|
||
transform string, `animate={{ transform: "translateX(100px)" }}`.
|
||
- **Don't drive child transforms via a CSS variable on the parent** — it recalcs
|
||
styles for all children. Set `transform` directly on the element.
|
||
- CSS (and WAAPI) beat rAF-based JS under load — use CSS for predetermined
|
||
motion, JS/springs for dynamic and gesture-driven motion.
|
||
- Keep transition-time `filter: blur()` under 20px — heavy blur is expensive,
|
||
especially in Safari.
|
||
|
||
Hunt for: `transition: all`, animated layout properties, Framer Motion shorthand
|
||
props on busy pages, `setProperty('--x', …)` driving child transforms, rAF loops
|
||
doing what CSS could.
|
||
|
||
## 6. Accessibility
|
||
|
||
```css
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.element {
|
||
animation: fade 0.2s ease;
|
||
} /* keep opacity/color, drop movement */
|
||
}
|
||
@media (hover: hover) and (pointer: fine) {
|
||
.element:hover {
|
||
transform: scale(1.05);
|
||
} /* touch fires false hovers on tap */
|
||
}
|
||
```
|
||
|
||
Reduced motion means fewer and gentler animations, **not zero** — keep
|
||
transitions that aid comprehension, remove position changes. In JS:
|
||
`useReducedMotion()` and branch transform values.
|
||
|
||
Hunt for: movement with no `prefers-reduced-motion` handling, ungated `:hover`
|
||
motion, reduced-motion implementations that nuke all feedback.
|
||
|
||
## 7. Cohesion & tokens
|
||
|
||
- Motion should match the product's personality — playful can be bouncier, a
|
||
dashboard stays crisp. Mismatched personality across components is a finding.
|
||
- Curves and durations should live as shared tokens. Five hand-typed
|
||
cubic-beziers that almost match is a consolidation finding.
|
||
- Everything-at-once group entrances where a **30–80ms stagger** belongs.
|
||
Stagger is decorative — it must never block interaction.
|
||
- A jarring crossfade that shows two overlapping states can be masked with
|
||
subtle `filter: blur(2px)` during the transition.
|
||
|
||
Hunt for: duplicated near-identical easings/durations, one bouncy component in a
|
||
crisp app, list/grid entrances with no stagger, crossfades that visibly
|
||
double-expose.
|
||
|
||
## 8. Missed opportunities
|
||
|
||
The additive category — places that don't animate but should:
|
||
|
||
- State changes that teleport (content swaps, layout jumps) where a brief
|
||
transition would prevent a jarring change.
|
||
- Spatially-connected UI (a panel that appears from a trigger) with no motion
|
||
explaining where it came from.
|
||
- Rare, high-emotion moments (first-run, success, celebration) rendered with
|
||
none of the delight budget they're allowed.
|
||
- `translate` percentages (`translateY(100%)` = element's own height) and
|
||
`clip-path: inset()` reveals as tools for these — no hardcoded pixel offsets.
|
||
|
||
Report at most a handful, grounded in actual UX seams you observed — not a
|
||
wishlist.
|