Frontend Animation
You are responsible for designing, implementing, reviewing, and optimizing frontend animations. By default, assume the project is React + TypeScript; for pure JS or other frameworks, follow the project's current setup—do not force React solutions on Vue or vanilla projects.
Animation is enhancement, not decoration: first ensure the static version looks good and is usable, then add animation.
Do not add any new dependencies without user confirmation.
§0 Prerequisites: Dependency & Design System Detection
Before starting, complete the following five steps in order; if any step is missing, detect it first before writing code.
- Read
package.jsonand the actual locked versions inpnpm-lock.yaml/package-lock.json/yarn.lockto confirm installed animation libraries and their major versions. - Read
tailwind.config.*, global styles (globals.css/index.css/app.css), and theme files to identify existing tokens, durations, easings, and naming conventions. - If motion exists, do not use GSAP; if GSAP exists, do not add motion. Never have two animation libraries for the same need.
- Do not add new dependencies without permission. If a new dependency is truly needed, first explain the package name, min+gzip size, alternatives, and why the existing library cannot do it, then wait for user confirmation before installing.
- Follow the project's existing naming, file organization, code style, and token names—do not introduce a second animation system.
| Detected | Default approach |
|---|---|
| Existing motion | Use motion for interactions, enter/exit, and layout animations; do not introduce GSAP |
| Existing framer-motion (older version) | Keep the existing package name and API; do not migrate to motion without explicit request; upgrade separately |
| Existing GSAP | Use GSAP + ScrollTrigger for timelines and scroll-linked animations; use CSS transitions for simple hovers—do not add motion |
| Only Tailwind | Use transition / duration / ease utility classes with global CSS variables; do not install a library |
| Pure CSS project | Use only CSS transitions/animations + WAAPI; do not install JS animation libraries |
| Next.js App Router | Mark animation components with "use client". Handle View Transitions and motion as client components; do not use cross-route shared state hacks |
| Vue | Use native CSS or Vue ecosystem motion solutions; do not force React-specific libraries into the project |
§1 Anti-Pattern Checklist (Do Not Violate)
Check each item. If any is violated, revert and rewrite—no "just this once" compromises.
| Forbidden | Alternative |
|---|---|
transition: all .3s | Explicitly list transform, opacity, and add color if needed |
Global ease-in-out for everything | Choose curves based on context: enter / exit / bidirectional (see §3) |
Element popping in from opacity: 0 + scale(.8) | Use opacity: .9→1 + translateY(8px)→0 |
Blind use of linear | Only for progress bars and infinite loop decorations |
| More than 3 elements animating simultaneously on the same screen | Keep only 1 visual focus; others remain static or very subtle |
Hover with transform: translateY(-4px) but parent container has no reserved space | Use scale or add padding to the parent |
| Only implement enter animation, no exit | Must design enter and exit in pairs, with exit faster than enter |
Large-area filter: blur() / drop-shadow animation | Use pre-rendered blurred layers, or reduce the animation scope |
| Animation layer covering CTA / forms | Set pointer-events: none on the animation layer, or keep it away from interactive areas |
| Page splash screen loading animation exceeding 1.5s | If it exceeds, show a skeleton screen first |
| Custom cursor + trailing effect (unless explicitly requested by user) | Do not do it |
| Complex per-word or per-frame animation on every list item | Use a 30–50ms stagger with a collective translate |
Chaining animations with setTimeout | Use transition/animation events, onAnimationComplete, or a timeline |
Per-frame el.style.left = x | Use WAAPI / rAF / library timeline |
Rationale for thresholds: When the first screen wait exceeds 1.5s, users start to suspect the page is frozen, so show a skeleton screen first. When more than 3 independent motions occur on the same screen, visual focus is lost and users don't know where to look.
§2 Decision Tree (Abbreviated)
First, determine the approach using the table below, then go to §3 to set values, and only then write code.
| Scenario | First choice | Should not use |
|---|---|---|
| Hover / press / toggle feedback | CSS transition | GSAP (too heavy) |
| Component enter/exit, list add/remove, layout changes | motion (AnimatePresence + layout) | Manual height animation |
| Route/page transitions | View Transitions API, or motion layoutId | Full-screen overlay + delay |
| Complex timelines, sequence orchestration, scroll-linked animations | GSAP + ScrollTrigger | CSS keyframes pile-up |
| 3D / particles / physics / shaders | three.js + React Three Fiber + drei | Faking 3D with CSS |
| Designer-provided vector animations | dotLottie / Lottie | Convert to GIF |
| Simple icons or path animations | SVG + CSS | Introducing Lottie runtime |
| Data chart animations | Library's built-in animation (e.g., Recharts/visx transitions) | Hand-tweening paths |
| Background decorations (grids/waves/glows) | CSS or canvas + requestAnimationFrame, and pause-able | three.js (unless you really need 3D) |
| Number/amount changes | tabular-nums + simple tween | Per-frame layout reflow |
For full decision logic, question ordering, and the "CSS → light library → heavy library" upgrade ladder, see references/decision-tree.md.
§3 Value Specifications (No Guesswork Allowed)
All durations, curves, and amplitudes must be taken from the following tables. If a value is not listed, first justify it before adding it.
Duration
| Item | Value |
|---|---|
| Hover / press | 120–180ms |
| Toggle / expand-collapse | 200–300ms |
| Enter | 240–400ms |
| Exit | 60–70% of enter duration |
| Page transition | 300–500ms |
| Layout reorder (layout animation) | 300–500ms |
| Stagger interval | 30–50ms, total group duration ≤ 500ms |
| Over 600ms | Must justify reason, otherwise cut it |
Easing
| Easing | Curve |
|---|---|
| Enter | cubic-bezier(0.16, 1, 0.3, 1) |
| Exit | cubic-bezier(0.4, 0, 1, 1) |
| Bidirectional / state toggle | cubic-bezier(0.4, 0, 0.2, 1) |
| Translation / drag / gesture | spring: stiffness 300–500, damping 30–40 |
| Linear | Only for progress bars, infinite loop decorations |
Amplitude
| Amplitude | Value |
|---|---|
| Translation | Micro 4–8px / Medium 12–24px / Large 32–64px / >80px only for full-screen transitions |
| Opacity | Micro-interaction .9→1; Enter .6→1; 0→1 only for full-screen overlays and modals |
| Scale | Only within 0.96–1.04 range |
Spring
| Parameter | Value | Rationale |
|---|---|---|
| stiffness | 300–500 | Below 300 is sluggish; above 500 shakes like a fault |
| damping | 30–40 | Below 30 overshoots too much; above 40 feels no bounce |
| mass | 0.8–1 | Minimal change to make inertia perceptible |
Rationale for each interval, one sentence each:
- Hover / press 120–180ms: Below 120ms the state change is imperceptible; above 180ms feedback is laggy, and users feel they missed the target.
- Enter 240–400ms: Below 240ms the movement direction is unreadable; above 400ms users start waiting.
- Exit × 0.65: Departures don't need to be clearly seen; slow means sluggish.
- Stagger 30–50ms: Below 30ms it looks like everything appears together; above 50ms the group exceeds the 500ms patience threshold.
These values have been encoded as CSS variables and TypeScript constants; see references/tokens.md. All code must reference tokens—hardcoded numbers are forbidden.
§4 Performance & Accessibility Red Lines
- Only animate
transform/opacity. To animate dimensions, use FLIP or the library'slayoutcapability. - For layers that will be continuously animating, add
will-change: transformand remove it after the animation ends. - Virtualize long lists; use IntersectionObserver to pause animations on off-screen elements.
prefers-reduced-motion: reduceis a mandatory branch, not a nice-to-have. The fallback must still show clear state changes: typically degrade to opacity crossfade ≤120ms (120ms is enough to read the state switch without requiring understanding of animation), or direct switch.- Do not use flashing more than 3 times per second (WCAG 2.3.1 photosensitivity threshold is 3 flashes per second).
- Do not steal focus or disturb tab order during animation; keyboard focus rings should not participate in animation.
- Design with the principle that "the static version should also look good"; animation is only an enhancement.
For WCAG guidelines, aria-live, and testing methods, see references/accessibility.md.
§5 Implementation Laws
- Design enter and exit in pairs, with exit faster.
- Keep only one visual focus per screen.
- Use 30–50ms stagger, total group ≤ 500ms.
- Decorative animations must have
pointer-events: none. - Chain animations using transition/animation events,
onAnimationComplete, or a timeline—neversetTimeout. - If CSS or Framer can do it, don't use GSAP.
- If
transformworks, don't animatewidth/height. - Submit animation duration to design review; don't adjust by "feeling slow".
- Subtract first, then add: remove animations you can, then optimize the ones you can't.
§6 Review Mode
When reviewing existing animations, output each issue in the following fixed four-part format:
Problem / Evidence (cite clauses from §1–§4) / Suggested code / Impact (duration, performance, accessibility)
Rules:
- Report only real problems. Don't rewrite reasonable implementations just to fill space.
- Maximum 8 items, sorted by severity: accessibility > performance > duration/curve > style.
- End with a "These are correct" section listing 1–3 items to prevent the user from mistakenly changing good implementations.
Scan command (read-only, no --fix; any automatic change to animations would break design intent):
node ~/.claude/skills/frontend-animation/scripts/audit-animation.mjs <project-directory> --json
The script only reports problems and locations; fixes are decided by humans.
§7 What to Read When You Need X
| Symptom / Need | File |
|---|---|
| Unsure whether to use CSS or a library, need to choose between frameworks, need to ask questions in order | references/decision-tree.md |
| Need to apply tokens, integrate with Tailwind, write CSS variables or TS constants | references/tokens.md |
| Look up installation, API, version pitfalls for motion / GSAP / three.js+R3F / Lottie / View Transitions | references/libraries.md |
| Need ready-made templates (list enter / route transition / drag / mouse-follow / number scroll / 3D flip / cyclic background / skeleton screen / magnetic button / Lottie) | references/patterns.md |
Choose easing curves, need linear() approximation for spring, explain why a certain curve is used | references/easing.md |
| reduced-motion fallback, WCAG, flashing and focus, testing methods | references/accessibility.md |
| Need a reference component with full reduced-motion branch | assets/template-component.tsx |
| Need to scan existing code for animation issues | scripts/audit-animation.mjs |
§8 Self-Check Before Commit (8 Questions, Answer Each)
- Is this animation necessary? Does the static version still look good without it?
- Are we only animating transform / opacity?
- Are duration, curve, and amplitude all within §3?
- Are enter and exit designed as a pair, with exit faster?
- Is there a reduced-motion branch, and can the state still be discerned after degradation?
- Have we added any new dependencies? Doing so without confirmation is a violation.
- Is there any flashing >3 times/second, or focus stealing during animation?
- Is there only one visual focus on the screen?
If any question cannot be answered, stop and fix it—do not commit.