web-animation-css-animations · diff
git:20260320.766fb9e to git:20260906.d80c3e7
141 added, 242 removed. Audit A to A.
---
name: web-animation-css-animations
- description: CSS Animation patterns - transitions, keyframes, scroll-driven animations, @property, GPU-accelerated properties, accessibility with prefers-reduced-motion
+ description: CSS animation patterns - transitions, keyframes, scroll-driven timelines, @property, compositor-friendly properties, prefers-reduced-motion
---
# CSS Animation Patterns
- > **Quick Guide:** Use CSS transitions for state changes (hover, focus), `@keyframes` for autonomous/looping animations, scroll-driven animations for scroll-linked effects. Animate only `transform` and `opacity` for 60fps. Always respect `prefers-reduced-motion`.
+ > **Quick Guide:** Transitions carry state changes (hover, focus, a toggled attribute); `@keyframes`
+ > carries motion that loops, auto-plays, or has more than two steps; `animation-timeline` carries
+ > scroll- and viewport-linked progress. Confining animation to `transform` and `opacity` keeps the
+ > frames on the compositor thread, and every animation gets a `prefers-reduced-motion` branch.
+ **Detailed Resources:**
+
+ - [examples/core.md](examples/core.md) — token system, interactive states, entrance, spinner, toast, reduced-motion
+ - [examples/transitions.md](examples/transitions.md) — multi-property transitions, staggered delays, accordions, colour, links
+ - [examples/keyframes.md](examples/keyframes.md) — scroll-driven timelines, `@property` gradients, typewriter, stagger, clip-path morphs
+ - [reference.md](reference.md) — easing catalogue, property cost table, duration guidance, browser support
+
---
- <critical_requirements>
+ ## Which path applies
- ## CRITICAL: Before Using This Skill
+ - **The motion is a state change** — `:hover`, `:focus-visible`, a data attribute, a toggled class —
+ then a `transition` on the base rule is the whole mechanism; follow
+ [examples/transitions.md](examples/transitions.md).
+ - **The motion loops, auto-plays on mount, or passes through more than two states** — then it needs
+ `@keyframes` and an `animation` shorthand; follow [examples/keyframes.md](examples/keyframes.md).
+ - **The motion tracks scroll position or viewport entry** — then the driver is
+ `animation-timeline: scroll()` or `view()` rather than time, and the keyframes describe progress
+ from 0 to 1; follow [examples/keyframes.md](examples/keyframes.md).
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ---
- **(You MUST animate ONLY transform and opacity for GPU-accelerated 60fps performance)**
+ <critical_requirements>
- **(You MUST respect prefers-reduced-motion using @media (prefers-reduced-motion: no-preference) for opt-in or @media (prefers-reduced-motion: reduce) for opt-out)**
+ ## Before writing CSS animation code
- **(You MUST use CSS custom properties for ALL timing values - NO magic numbers like `0.3s`)**
+ **Animate `transform` and `opacity`.** Both are composited, so the frames run off the main thread and
+ survive a busy tab; `width`, `top` and `margin` re-run layout on every frame instead.
- **(You MUST use ease-out for enter animations and ease-in for exit animations - NEVER linear for UI transitions)**
+ **Give every animation a `prefers-reduced-motion` branch.** The preference is a vestibular safety
+ setting rather than an off switch — an opacity fade at a shorter duration usually satisfies it while
+ keeping the state change legible.
- **(You MUST remove will-change after animation completes - permanent will-change wastes GPU memory)**
+ **Use `ease-out` on enter and `ease-in` on exit.** An element arriving decelerates into place and one
+ leaving accelerates away; `linear` reads as mechanical for anything but continuous rotation.
+ **Scope `will-change` to the interaction that needs it.** Each declaration holds a compositing layer
+ for as long as the rule applies, so a blanket selector holds one per element on the page at once.
+
</critical_requirements>
---
- **Auto-detection:** CSS animation, CSS transition, @keyframes, transform, opacity, transition-duration, animation-duration, prefers-reduced-motion, scroll-timeline, animation-timeline, will-change, cubic-bezier, ease-out, ease-in, @property
-
- **When to use:**
-
- - Simple state change animations (hover, focus, active states)
- - Autonomous looping animations (spinners, pulses, attention grabbers)
- - Scroll-linked animations and parallax effects
- - Micro-interactions that don't need JavaScript control
+ **Auto-detection:** @keyframes, transition-property, transition-duration, animation-timeline,
+ scroll-timeline, view-timeline, animation-range, animation-fill-mode, prefers-reduced-motion,
+ will-change, cubic-bezier, linear(), @property, steps(), transform-origin
- **When NOT to use:**
+ **Applies to:**
- - Animations requiring JavaScript control (pause, reverse, seek) -- use Web Animations API
- - Complex orchestrated animations with staggered timing -- use your animation library
- - Physics-based spring animations -- use your animation library
- - Drag-and-drop or gesture-driven animations -- use your animation library
+ - State-change motion driven by a pseudo-class, a data attribute or a toggled class
+ - Autonomous motion — spinners, pulses, shimmer, attention cues
+ - Scroll-linked and viewport-entry progress
+ - Entrance and exit motion whose trigger is a class or attribute the page already sets
- **Detailed Resources:**
+ **Handled elsewhere:**
- - [examples/core.md](examples/core.md) - Token system, interactive states, shadows, loading, reduced motion
- - [examples/transitions.md](examples/transitions.md) - Multi-property transitions, accordions, color, links
- - [examples/keyframes.md](examples/keyframes.md) - Scroll-driven, @property gradients, typewriter, stagger, shapes
- - [reference.md](reference.md) - Decision frameworks, timing reference, browser support
+ - Playback control at runtime — pause, reverse, seek, or read progress. A CSS declaration exposes no
+ handle; the Web Animations API is where one comes from, either `element.animate()` or
+ `element.getAnimations()` over what CSS already declared
+ - Motion whose velocity carries across an interruption, such as a spring picked up mid-gesture
+ - Pointer-tracking drag, where the animated value is the pointer position itself
+ - Compositing an outgoing and an incoming view together across a navigation or view swap
---
- <philosophy>
+ <decision_framework>
- ## Philosophy
+ ## Easing selection
- CSS animations leverage the browser's compositor thread for smooth, 60fps animations that don't block JavaScript execution. By animating only GPU-accelerated properties (`transform` and `opacity`), animations run on a separate thread from the main JavaScript thread.
+ ```
+ Element entering -> ease-out (fast start, slow settle)
+ Element exiting -> ease-in (slow start, fast departure)
+ Symmetric motion -> ease-in-out
+ Continuous rotation -> linear
+ Playful, overshooting -> cubic-bezier with a control point past 1
+ Anything else -> ease-out
+ ```
- **Core principles:**
+ `ease`, the browser default, is generic enough that two adjacent animations using it read as
+ unrelated; name the curve instead.
- 1. **Performance first** - Animate only `transform` and `opacity` to avoid layout/paint triggers
- 2. **Accessibility built-in** - Always respect `prefers-reduced-motion` user preferences
- 3. **Transitions for state changes** - Use CSS transitions for hover, focus, and state-driven animations
- 4. **Keyframes for autonomous motion** - Use `@keyframes` for animations that loop, auto-play, or have multiple steps
- 5. **Design tokens for consistency** - Use CSS custom properties for durations, easings, and distances
+ ## What CSS expresses
- </philosophy>
+ - **Scroll and viewport progress** — `animation-timeline: scroll()` or `view()`, with
+ `animation-range` deciding where progress starts and ends
+ - **Sequencing across elements** — `animation-delay` computed from an `--index` custom property, with
+ `backwards` fill so the pre-animation state holds during the delay
+ - **Values computed at runtime** — write them into a custom property; the animation itself stays
+ declarative and reads the property each frame
+ - **Overshoot and arbitrary curves** — a `cubic-bezier` past the 0–1 range, or `linear()` with a
+ point list for a curve no cubic can express
+ </decision_framework>
+
---
<patterns>
- ## Core Patterns
+ ## Core patterns
### Pattern 1: Animation Token System
- Define timing, easing, and distance tokens as CSS custom properties for consistency. See [examples/core.md](examples/core.md) for the full token setup.
+ Durations, easings and travel distances defined once as custom properties, so motion stays consistent
+ across components and is retunable in one place.
```css
:root {
- --duration-instant: 100ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
- --duration-slow: 400ms;
-
- --ease-out: cubic-bezier(0, 0, 0.2, 1); /* Enter */
- --ease-in: cubic-bezier(0.4, 0, 1, 1); /* Exit */
- --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* Symmetric */
- --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Bouncy */
-
- --lift-sm: -2px;
+ --ease-out: cubic-bezier(0, 0, 0.2, 1); /* enter */
+ --ease-in: cubic-bezier(0.4, 0, 1, 1); /* exit */
+ --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* overshoot */
--lift-md: -4px;
}
```
- **Why tokens matter:** Consistent timing across application, easy to adjust globally, semantic naming communicates intent
+ Full code: [examples/core.md](examples/core.md)
---
- ### Pattern 2: GPU-Accelerated Transitions
+ ### Pattern 2: Compositor-Only Transitions
- Only animate `transform` and `opacity`. Never animate layout properties like `width`, `height`, `top`, `left`, `margin`, or `padding`.
+ Name each property being transitioned, and express movement and size as `transform` so no frame
+ triggers layout.
```css
- /* CORRECT - GPU-accelerated */
.card {
transition:
transform var(--duration-fast) var(--ease-out),
opacity var(--duration-fast) var(--ease-out);
}
.card:hover {
transform: translateY(var(--lift-md)) scale(1.02);
}
```
- ```css
- /* WRONG - triggers layout recalculation every frame */
- .card {
- transition: all 0.3s linear;
- }
- .card:hover {
- top: -8px;
- margin-top: -8px;
- }
- ```
-
- **Transform mapping:** Use `translate()` instead of `top/left`, `scale()` instead of `width/height`, pseudo-element opacity instead of `box-shadow`.
+ `translate()` replaces `top`/`left`, `scale()` replaces `width`/`height`, and a pseudo-element whose
+ `opacity` animates replaces an animated `box-shadow`.
- See [examples/core.md](examples/core.md) for button states, card hover effects, and the pseudo-element shadow technique.
+ Full code: [examples/core.md](examples/core.md)
---
### Pattern 3: Prefers-Reduced-Motion
- Every animation must respect user motion preferences. Two strategies:
-
- #### Progressive Enhancement (Recommended)
+ Two shapes. Progressive enhancement makes the still state the base and opts motion in, so an
+ animation added later cannot escape the check:
```css
- /* Base: no motion */
.element {
opacity: 1;
transform: translateY(0);
}
- /* Opt-in to motion */
@media (prefers-reduced-motion: no-preference) {
.element {
animation: fade-slide-in var(--duration-normal) var(--ease-out);
}
}
```
- #### Graceful Degradation
+ Graceful degradation animates by default and overrides under `reduce` — the right shape when the
+ reduced form is a shorter fade rather than nothing:
```css
- .notification {
- animation: slide-in-bounce var(--notification-duration) var(--ease-spring);
- }
-
@media (prefers-reduced-motion: reduce) {
.notification {
animation: fade-in calc(var(--notification-duration) * 0.5) var(--ease-out);
}
}
```
- **Key insight:** Reduced motion does not mean no animation. Opacity fades are generally safe. Replace spatial movement with opacity-only alternatives.
+ Reduced motion does not mean no animation. Opacity is generally safe; what it replaces is spatial
+ travel, scale and rotation.
- See [examples/core.md](examples/core.md) for the complete reduced motion pattern.
+ Full code: [examples/core.md](examples/core.md)
---
- ### Pattern 4: CSS @keyframes
+ ### Pattern 4: @keyframes
- Use `@keyframes` for animations that loop, auto-play on mount, or have more than two states.
+ For motion that loops, auto-plays on mount, or passes through more than two states.
```css
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal {
- --modal-enter-duration: 300ms;
animation: fade-slide-in var(--modal-enter-duration) var(--ease-out) forwards;
}
```
- **Key details:**
-
- - Use `forwards` fill mode to retain final state after animation
- - Use `backwards` fill mode to show initial state during `animation-delay`
- - Use `ease-out` for enter, `ease-in` for exit
- - `linear` is only appropriate for continuous rotation (spinners)
+ `forwards` holds the final state after the run; `backwards` shows the initial state during
+ `animation-delay`.
- See [examples/core.md](examples/core.md) for spinners, pulses, skeleton loaders, and toast animations. See [examples/keyframes.md](examples/keyframes.md) for scroll-driven animations, @property gradients, and complex sequences.
+ Full code: [examples/core.md](examples/core.md) and
+ [examples/keyframes.md](examples/keyframes.md)
---
- ### Pattern 5: Will-Change Optimization
+ ### Pattern 5: Will-Change Scoping
- `will-change` creates a GPU layer (~307KB per 320x240px element). Apply only when needed, remove after.
+ `will-change` promotes the element to its own compositing layer, which costs GPU memory
+ proportional to the element's painted area. Declare it on the rule that is about to animate.
```css
- /* CORRECT - only during interaction */
.card:hover {
will-change: transform;
}
-
- /* WRONG - permanent GPU layer on every element */
- * {
- will-change: transform;
- }
```
- Never apply `will-change` permanently. Each element with `will-change` creates a separate compositing layer that consumes GPU memory. On mobile devices, this can crash the browser.
+ Full code: [examples/core.md](examples/core.md)
---
### Pattern 6: Scroll-Driven Animations
- CSS `animation-timeline` allows scroll-linked animations without JavaScript.
+ `animation-timeline` drives keyframes from scroll progress instead of elapsed time, with no
+ scroll listener and no per-frame JavaScript.
```css
.progress-bar {
animation: grow-width linear;
animation-timeline: scroll();
}
@keyframes grow-width {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
```
- **Two timeline types:**
-
- - `scroll()` -- progress based on scroll container position
- - `view()` -- progress based on element visibility in viewport
+ `scroll()` tracks a scroll container's position; `view()` tracks the element's own passage through
+ the viewport, with `animation-range` bounding it.
- **Browser support:** Chrome/Edge 115+, Safari 26+, Firefox behind flag
+ **Browser support:** Chrome/Edge 115+, Safari 26+, Firefox behind a flag.
- See [examples/keyframes.md](examples/keyframes.md) for scroll progress, viewport reveal, and parallax patterns.
+ Full code: [examples/keyframes.md](examples/keyframes.md)
---
### Pattern 7: @property for Custom Property Animation
- CSS Houdini's `@property` enables animating custom properties like gradient angles that CSS cannot normally interpolate.
+ Registering a custom property gives it a type, which is what makes it interpolable — gradient angles
+ and colour stops animate only once registered.
```css
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
- .gradient-border {
- background: linear-gradient(var(--gradient-angle), #ff0080, #7928ca);
- animation: rotate-gradient 3s linear infinite;
- }
-
@keyframes rotate-gradient {
to {
--gradient-angle: 360deg;
}
}
```
- **Browser support:** Chrome/Edge 85+, Safari 16.4+, Firefox 128+
-
- </patterns>
-
- ---
-
- <performance>
-
- ## Performance
-
- ### The 16.67ms Budget
-
- For 60fps, each frame must complete in 16.67ms. Layout-triggering animations often exceed this budget.
-
- | Category | Properties | Impact |
- | -------------------------- | ----------------------------------------- | ------------------------------------ |
- | **Composite only (Best)** | transform, opacity | No layout, no paint, GPU-accelerated |
- | **Paint only (Okay)** | color, background-color, visibility | No layout, but repaints |
- | **Layout + Paint (Avoid)** | width, height, margin, padding, top, left | Full page recalculation |
-
- ### Duration Guidelines
-
- | Animation Type | Duration | Reason |
- | ------------------ | ---------- | ------------------------- |
- | Micro-interactions | 100-150ms | Feels instant |
- | UI transitions | 200-300ms | Sweet spot for perception |
- | Page transitions | 300-500ms | Major context change |
- | Complex sequences | 500-1000ms | Story-telling moments |
-
- ### Transform Mapping
-
- | Instead of... | Use... |
- | ------------------- | ------------------------------- |
- | `top`, `left` | `translate(x, y)` |
- | `width`, `height` | `scale()` |
- | `box-shadow` | Pseudo-element with opacity |
- | `margin`, `padding` | `translate()` with layout space |
-
- </performance>
-
- ---
-
- <decision_framework>
-
- ## Decision Framework
-
- ### Transitions vs @keyframes
-
- ```
- Is the animation triggered by user interaction (hover, focus, class toggle)?
- ├─ YES → Is it a simple A->B state change?
- │ ├─ YES -> CSS Transition
- │ └─ NO -> Does it need multiple steps?
- │ ├─ YES -> CSS @keyframes
- │ └─ NO -> CSS Transition is fine
- └─ NO -> Does it auto-play or loop?
- ├─ YES -> CSS @keyframes
- └─ NO -> CSS Transition (triggered by class toggle)
- ```
-
- ### Easing Selection
-
- ```
- What type of motion?
- ├─ Element entering -> ease-out (fast start, slow end)
- ├─ Element exiting -> ease-in (slow start, fast end)
- ├─ Symmetric motion -> ease-in-out
- ├─ Continuous rotation -> linear
- ├─ Playful/bouncy -> custom cubic-bezier with overshoot
- └─ Default UI -> ease-out
-
- Never use:
- ├─ linear for UI transitions (feels robotic)
- └─ ease (browser default) for production (too generic)
- ```
-
- ### CSS vs JavaScript Animation
+ **Browser support:** Chrome/Edge 85+, Safari 16.4+, Firefox 128+.
- ```
- Does the animation need...
- ├─ Pause/play/reverse/seek control? -> JavaScript (Web Animations API)
- ├─ Dynamic values calculated at runtime? -> JavaScript or CSS custom properties
- ├─ Physics-based springs? -> Your animation library
- ├─ Orchestrated staggering across many elements? -> JavaScript for complex, CSS for simple
- ├─ Scroll-linked progress? -> CSS scroll-driven animations
- ├─ Page/view transitions? -> See the View Transitions skill
- └─ Simple state transitions? -> CSS Transitions
- ```
+ Full code: [examples/keyframes.md](examples/keyframes.md)
- </decision_framework>
+ </patterns>
---
<red_flags>
- ## RED FLAGS
-
- ### High Priority
-
- - **Animating layout properties** (`width`, `height`, `top`, `left`, `margin`, `padding`) -- triggers expensive reflows every frame; use `transform` instead
- - **Magic numbers for timing** (`0.3s`, `300ms` inline) -- all durations must be CSS custom properties
- - **Missing `prefers-reduced-motion`** -- every animation must respect user preferences
- - **Linear easing for UI transitions** -- `linear` feels robotic; use `ease-out` for enter, `ease-in` for exit
- - **Permanent `will-change`** -- creates GPU layers permanently, wasting memory; apply only during animation
+ ## Red flags
- ### Medium Priority
+ **Breaks at runtime:**
- - **Using `transition: all`** -- transitions unnecessary properties, causes surprises when new properties are added
- - **Animating `box-shadow` directly** -- causes repaint every frame; use pseudo-element with opacity
- - **Missing `forwards` on enter animations** -- element snaps back to initial state
- - **Very long durations (>1s)** -- users perceive as slow; rarely appropriate outside special effects
+ - `transition: all` — picks up every property a later edit adds, including layout-triggering ones —
+ name each transitioned property explicitly
+ - Animating `width`, `height`, `top`, `left`, `margin` or `padding` — re-runs layout every frame and
+ drops frames as soon as the main thread is busy — animate `transform` and leave layout still
+ - Animating `box-shadow` — repaints the element and its shadow each frame — animate the `opacity` of
+ a pseudo-element that carries the shadow
+ - `will-change` on a permanent or broad selector — holds one compositing layer per matched element,
+ and on mobile enough layers exhaust GPU memory and kill the tab — declare it on the interaction
+ rule only
+ - An animation with no `prefers-reduced-motion` branch — full-travel motion reaches users who have
+ asked their OS for none — add the branch when the animation is written, not afterwards
+ - An enter animation without `forwards` — the element snaps back to its pre-animation state on the
+ final frame — add the fill mode
- ### Gotchas & Edge Cases
+ **Surprising behaviour:**
- - **`transform` + `position: fixed`** -- transform creates new containing block, breaking fixed positioning relative to viewport
- - **`will-change` creates stacking context** -- can affect z-index behavior unexpectedly
- - **Cannot animate `display: none`** -- use `opacity` + `visibility` or `grid-template-rows: 0fr`
- - **`fill-mode: backwards` needed for delayed animations** -- without it, element shows in final state during delay
- - **SVG uses different properties** -- animate `stroke-dashoffset` and `stroke-dasharray`, not `transform` for path drawing
- - **Scroll-driven animations need scrollable container** -- `overflow: hidden` parent breaks `scroll-timeline`
- - **Print media** -- animations don't print; ensure content is visible without animation
+ - `transform` on an ancestor creates a containing block, so a `position: fixed` descendant anchors to
+ that ancestor rather than to the viewport
+ - `will-change` creates a stacking context, changing how `z-index` resolves against siblings
+ - Without `animation-fill-mode: backwards`, a delayed animation renders its final state during the
+ delay instead of its first frame
+ - `display: none` cannot be animated — use `opacity` with `visibility`, or `grid-template-rows`
+ animating `0fr` to `1fr`
+ - SVG path drawing animates `stroke-dasharray` and `stroke-dashoffset`; `transform` moves the path
+ rather than drawing it
+ - A `scroll()` timeline needs a scrollable ancestor — an `overflow: hidden` parent yields no progress
+ - Animations do not run in print, so the pre-animation state has to be legible on paper
+ - Durations past roughly 1s read as sluggish rather than deliberate
</red_flags>
-
- ---
-
- <critical_reminders>
-
- ## CRITICAL REMINDERS
-
- > **All code must follow project conventions in CLAUDE.md**
-
- **(You MUST animate ONLY transform and opacity for GPU-accelerated 60fps performance)**
-
- **(You MUST respect prefers-reduced-motion using @media (prefers-reduced-motion: no-preference) for opt-in or @media (prefers-reduced-motion: reduce) for opt-out)**
-
- **(You MUST use CSS custom properties for ALL timing values - NO magic numbers like `0.3s`)**
-
- **(You MUST use ease-out for enter animations and ease-in for exit animations - NEVER linear for UI transitions)**
-
- **(You MUST remove will-change after animation completes - permanent will-change wastes GPU memory)**
-
- **Failure to follow these rules will cause jank, accessibility issues, and degraded user experience.**
-
- </critical_reminders>