From 949101a1edcd0a06712b82f1faf0985d1aa4f256 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Mon, 24 Nov 2025 14:54:14 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20css-keyframes-animati?= =?UTF-8?q?ons=20skill=20for=20native=20CSS=20animations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new skill for creating performant CSS animations: - Native @keyframes animations with GPU acceleration - Angular animate.enter/leave for modern view transitions - Performance guidelines and best practices - Reference guide for common animation patterns --- .../skills/css-keyframes-animations/SKILL.md | 392 +++++++++ .../assets/animations.css | 278 ++++++ .../references/keyframes-guide.md | 833 ++++++++++++++++++ 3 files changed, 1503 insertions(+) create mode 100644 .claude/skills/css-keyframes-animations/SKILL.md create mode 100644 .claude/skills/css-keyframes-animations/assets/animations.css create mode 100644 .claude/skills/css-keyframes-animations/references/keyframes-guide.md diff --git a/.claude/skills/css-keyframes-animations/SKILL.md b/.claude/skills/css-keyframes-animations/SKILL.md new file mode 100644 index 000000000..58b12e548 --- /dev/null +++ b/.claude/skills/css-keyframes-animations/SKILL.md @@ -0,0 +1,392 @@ +--- +name: css-keyframes-animations +description: This skill should be used when writing or reviewing CSS animations in Angular components. Use when creating entrance/exit animations, implementing @keyframes instead of @angular/animations, applying timing functions and fill modes, creating staggered animations, or ensuring GPU-accelerated performance. Essential for modern Angular 20+ components using animate.enter/animate.leave directives and converting legacy Angular animations to native CSS. +--- + +# CSS @keyframes Animations + +## Overview + +Implement native CSS @keyframes animations for Angular applications, replacing @angular/animations with GPU-accelerated, zero-bundle-size alternatives. This skill provides comprehensive guidance on creating performant entrance/exit animations, staggered effects, and proper timing configurations. + +## When to Use This Skill + +Apply this skill when: +- **Writing Angular components** with entrance/exit animations +- **Converting @angular/animations** to native CSS @keyframes +- **Implementing animate.enter/animate.leave** in Angular 20+ templates +- **Creating staggered animations** for lists or collections +- **Debugging animation issues** (snap-back, wrong starting positions, choppy playback) +- **Optimizing animation performance** for GPU acceleration +- **Reviewing animation code** for accessibility and best practices + +## Quick Start + +### Basic Animation Setup + +1. **Define @keyframes** in component CSS: +```css +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +``` + +2. **Apply animation** to element: +```css +.element { + animation: fadeIn 0.3s ease-out; +} +``` + +3. **Use with Angular 20+ directives**: +```html +@if (visible()) { +
+ Content +
+} +``` + +### Common Pitfall: Element Snaps Back + +**Problem:** Element returns to original state after animation completes. + +**Solution:** Add `forwards` fill mode: +```css +.element { + animation: fadeOut 1s forwards; +} +``` + +### Common Pitfall: Animation Conflicts with State Transitions + +**Problem:** Entrance animation overrides initial state transforms (e.g., stacked cards appear unstacked then jump). + +**Solution:** Animate only properties that don't conflict with state. Use opacity-only animations when transforms are state-driven: +```css +/* BAD: Overrides stacked transform */ +@keyframes cardEntrance { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* GOOD: Only animates opacity, allows state transforms to apply */ +@keyframes cardEntrance { + from { opacity: 0; } + to { opacity: 1; } +} +``` + +## Core Principles + +### 1. GPU-Accelerated Properties Only + +**Always use** for animations: +- `transform` (translate, rotate, scale) +- `opacity` + +**Avoid animating** (triggers layout recalculation): +- `width`, `height` +- `top`, `left`, `right`, `bottom` +- `margin`, `padding` +- `font-size` + +### 2. Fill Modes + +| Fill Mode | Behavior | Use Case | +|-----------|----------|----------| +| `forwards` | Keep end state | Exit animations (stay invisible) | +| `backwards` | Apply start state during delay | Entrance with delay (prevent flash) | +| `both` | Both of above | Complex sequences | + +### 3. Timing Functions + +Choose appropriate easing for animation type: + +```css +/* Entrance animations - ease-out (fast start, slow end) */ +animation-timing-function: cubic-bezier(0, 0, 0.58, 1); + +/* Exit animations - ease-in (slow start, fast end) */ +animation-timing-function: cubic-bezier(0.42, 0, 1, 1); + +/* Bouncy overshoot effect */ +animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); +``` + +Tool: [cubic-bezier.com](https://cubic-bezier.com) for custom curves. + +### 4. Staggered Animations + +Create cascading effects using CSS custom properties: + +**Template:** +```html +@for (item of items(); track item.id; let idx = $index) { +
+ {{ item.name }} +
+} +``` + +**CSS:** +```css +.stagger-item { + animation: fadeSlideIn 0.5s ease-out backwards; + animation-delay: calc(var(--i, 0) * 100ms); +} +``` + +### 5. Accessibility + +Always respect reduced motion preferences: + +```css +@media (prefers-reduced-motion: reduce) { + .animated { + animation: none; + /* Or use simpler, faster animation */ + animation-duration: 0.1s; + } +} +``` + +## Common Animation Patterns + +### Fade Entrance/Exit + +```css +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +.fade-in { animation: fadeIn 0.3s ease-out; } +.fade-out { animation: fadeOut 0.3s ease-in forwards; } +``` + +### Slide Entrance + +```css +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.slide-in-up { animation: slideInUp 0.3s ease-out; } +``` + +### Scale Entrance + +```css +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.scale-in { animation: scaleIn 0.2s ease-out; } +``` + +### Loading Spinner + +```css +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid #f3f3f3; + border-top: 3px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} +``` + +### Shake (Error Feedback) + +```css +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } +} + +.error-input { + animation: shake 0.5s ease-in-out; +} +``` + +## Angular 20+ Integration + +### Basic Usage with animate.enter/animate.leave + +```typescript +@Component({ + template: ` + @if (show()) { +
+ Content +
+ } + `, + styles: [` + .fade-in { animation: fadeIn 0.3s ease-out; } + .fade-out { animation: fadeOut 0.3s ease-in forwards; } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } + } + `] +}) +``` + +### Dynamic Animation Classes + +```typescript +@Component({ + template: ` + @if (show()) { +
+ Content +
+ } + ` +}) +export class DynamicAnimComponent { + show = signal(false); + enterAnim = signal('slide-in-up'); + leaveAnim = signal('slide-out-down'); +} +``` + +## Debugging Animations + +### Common Issues + +| Problem | Cause | Solution | +|---------|-------|----------| +| Animation doesn't run | Missing duration | Add `animation-duration: 0.3s` | +| Element snaps back | No fill mode | Add `animation-fill-mode: forwards` | +| Wrong starting position during delay | No backwards fill | Add `animation-fill-mode: backwards` | +| Choppy animation | Animating layout properties | Use `transform` instead | +| State conflict (jump/snap) | Animation overrides state transforms | Animate only opacity, not transform | + +### Browser DevTools + +- **Chrome DevTools** → More Tools → Animations panel +- **Firefox DevTools** → Inspector → Animations tab + +### Animation Events + +```typescript +element.addEventListener('animationend', (e) => { + console.log('Animation completed:', e.animationName); + // Clean up, remove element, etc. +}); +``` + +## Resources + +### references/keyframes-guide.md + +Comprehensive deep-dive covering: +- Complete @keyframes syntax reference +- Detailed timing functions and cubic-bezier curves +- Advanced techniques (direction, play-state, @starting-style) +- Performance optimization strategies +- Extensive common patterns library +- Debugging techniques and troubleshooting + +**When to reference:** Complex animation requirements, custom easing curves, advanced techniques, performance optimization, or learning comprehensive details. + +### assets/animations.css + +Ready-to-use CSS file with common animation patterns: +- Fade animations (in/out) +- Slide animations (up/down/left/right) +- Scale animations (in/out) +- Utility animations (spin, shimmer, shake, breathe, attention-pulse) +- Toast/notification animations +- Accessibility (@media prefers-reduced-motion) + +**Usage:** Copy this file to project and import in component styles or global styles: + +```css +@import 'path/to/animations.css'; +``` + +Then use classes directly: +```html +
+``` + +## Migration from @angular/animations + +### Before (Angular Animations) + +```typescript +import { trigger, state, style, transition, animate } from '@angular/animations'; + +@Component({ + animations: [ + trigger('fadeIn', [ + transition(':enter', [ + style({ opacity: 0 }), + animate('300ms ease-out', style({ opacity: 1 })) + ]) + ]) + ] +}) +``` + +### After (CSS @keyframes) + +```typescript +@Component({ + template: ` + @if (show()) { +
Content
+ } + `, + styles: [` + .fade-in { animation: fadeIn 0.3s ease-out; } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + `] +}) +``` + +**Benefits:** +- Zero JavaScript bundle size (~60KB savings) +- GPU hardware acceleration +- Standard CSS (transferable skills) +- Better performance diff --git a/.claude/skills/css-keyframes-animations/assets/animations.css b/.claude/skills/css-keyframes-animations/assets/animations.css new file mode 100644 index 000000000..b285100e3 --- /dev/null +++ b/.claude/skills/css-keyframes-animations/assets/animations.css @@ -0,0 +1,278 @@ +/** + * Reusable CSS @keyframes Animations + * + * Common animation patterns for Angular applications. + * Import this file in your component styles or global styles. + */ + +/* ============================================ + FADE ANIMATIONS + ============================================ */ + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +.fade-in { + animation: fadeIn 0.3s ease-out; +} + +.fade-out { + animation: fadeOut 0.3s ease-in; +} + +/* ============================================ + SLIDE ANIMATIONS + ============================================ */ + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOutDown { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +} + +@keyframes slideInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOutUp { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-20px); + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOutLeft { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(-20px); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideOutRight { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(20px); + } +} + +.slide-in-up { animation: slideInUp 0.3s ease-out; } +.slide-out-down { animation: slideOutDown 0.3s ease-in; } +.slide-in-down { animation: slideInDown 0.3s ease-out; } +.slide-out-up { animation: slideOutUp 0.3s ease-in; } +.slide-in-left { animation: slideInLeft 0.3s ease-out; } +.slide-out-left { animation: slideOutLeft 0.3s ease-in; } +.slide-in-right { animation: slideInRight 0.3s ease-out; } +.slide-out-right { animation: slideOutRight 0.3s ease-in; } + +/* ============================================ + SCALE ANIMATIONS + ============================================ */ + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.9); + } +} + +.scale-in { animation: scaleIn 0.2s ease-out; } +.scale-out { animation: scaleOut 0.2s ease-in; } + +/* ============================================ + UTILITY ANIMATIONS + ============================================ */ + +/* Loading Spinner */ +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spin { + animation: spin 1s linear infinite; +} + +/* Skeleton Loading */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.shimmer { + background: linear-gradient( + 90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +/* Attention Pulse */ +@keyframes attention-pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5); + } + 50% { + box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); + } +} + +.attention-pulse { + animation: attention-pulse 2s ease-in-out infinite; +} + +/* Shake (Error Feedback) */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } +} + +.shake { + animation: shake 0.5s ease-in-out; +} + +/* Breathing/Pulsing */ +@keyframes breathe { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.05); + } +} + +.breathe { + animation: breathe 2s ease-in-out infinite; +} + +/* ============================================ + TOAST/NOTIFICATION ANIMATIONS + ============================================ */ + +@keyframes toastIn { + from { + opacity: 0; + transform: translateY(100%) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes toastOut { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(100%) scale(0.9); + } +} + +.toast-in { + animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.toast-out { + animation: toastOut 0.2s ease-in forwards; +} + +/* ============================================ + ACCESSIBILITY + ============================================ */ + +/* Respect user's motion preferences */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/.claude/skills/css-keyframes-animations/references/keyframes-guide.md b/.claude/skills/css-keyframes-animations/references/keyframes-guide.md new file mode 100644 index 000000000..378175a16 --- /dev/null +++ b/.claude/skills/css-keyframes-animations/references/keyframes-guide.md @@ -0,0 +1,833 @@ +# CSS @keyframes Deep Dive + +A comprehensive guide for Angular developers transitioning from `@angular/animations` to native CSS animations. + +--- + +## Table of Contents + +1. [Understanding @keyframes](#understanding-keyframes) +2. [Basic Syntax](#basic-syntax) +3. [Animation Properties](#animation-properties) +4. [Timing Functions (Easing)](#timing-functions-easing) +5. [Fill Modes](#fill-modes) +6. [Advanced Techniques](#advanced-techniques) +7. [Angular 20+ Integration](#angular-20-integration) +8. [Common Patterns & Recipes](#common-patterns--recipes) +9. [Performance Tips](#performance-tips) +10. [Debugging Animations](#debugging-animations) + +--- + +## Understanding @keyframes + +The `@keyframes` at-rule controls the intermediate steps in a CSS animation sequence by defining styles for keyframes (waypoints) along the animation. Unlike transitions (which only animate between two states), keyframes let you define multiple intermediate steps. + +### How It Differs from @angular/animations + +| @angular/animations | Native CSS @keyframes | +|---------------------|----------------------| +| ~60KB JavaScript bundle | Zero JS overhead | +| CPU-based rendering | GPU hardware acceleration | +| Angular-specific syntax | Standard CSS (transferable skills) | +| `trigger()`, `state()`, `animate()` | `@keyframes` + CSS classes | + +--- + +## Basic Syntax + +### The @keyframes Rule + +```css +@keyframes animation-name { + from { + /* Starting styles (same as 0%) */ + } + to { + /* Ending styles (same as 100%) */ + } +} +``` + +### Percentage-Based Keyframes + +For more control, use percentages to define multiple waypoints: + +```css +@keyframes bounce { + 0% { + transform: translateY(0); + } + 25% { + transform: translateY(-30px); + } + 50% { + transform: translateY(0); + } + 75% { + transform: translateY(-15px); + } + 100% { + transform: translateY(0); + } +} +``` + +### Combining Multiple Percentages + +You can apply the same styles to multiple keyframes: + +```css +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.05); + } +} +``` + +### Applying the Animation + +```css +.element { + animation: bounce 1s ease-in-out infinite; +} +``` + +--- + +## Animation Properties + +### Individual Properties + +| Property | Description | Example | +|----------|-------------|---------| +| `animation-name` | Name of the @keyframes | `animation-name: bounce;` | +| `animation-duration` | How long one cycle takes | `animation-duration: 2s;` | +| `animation-timing-function` | Speed curve (easing) | `animation-timing-function: ease-in;` | +| `animation-delay` | Wait before starting | `animation-delay: 500ms;` | +| `animation-iteration-count` | How many times to run | `animation-iteration-count: 3;` or `infinite` | +| `animation-direction` | Forward, reverse, or alternate | `animation-direction: alternate;` | +| `animation-fill-mode` | Styles before/after animation | `animation-fill-mode: forwards;` | +| `animation-play-state` | Pause or play | `animation-play-state: paused;` | + +### Shorthand Syntax + +```css +/* animation: name duration timing-function delay iteration-count direction fill-mode play-state */ +.element { + animation: slideIn 0.5s ease-out 0.2s 1 normal forwards running; +} +``` + +**Minimum required:** name and duration + +```css +.element { + animation: fadeIn 1s; +} +``` + +### Multiple Animations + +Apply multiple animations to a single element: + +```css +.element { + animation: + fadeIn 0.5s ease-out, + slideUp 0.5s ease-out, + pulse 2s ease-in-out 0.5s infinite; +} +``` + +--- + +## Timing Functions (Easing) + +The timing function controls how the animation progresses over time—where it speeds up and slows down. + +### Keyword Values + +| Keyword | Cubic-Bezier Equivalent | Description | +|---------|------------------------|-------------| +| `linear` | `cubic-bezier(0, 0, 1, 1)` | Constant speed | +| `ease` | `cubic-bezier(0.25, 0.1, 0.25, 1)` | Default: slow start, fast middle, slow end | +| `ease-in` | `cubic-bezier(0.42, 0, 1, 1)` | Slow start, fast end | +| `ease-out` | `cubic-bezier(0, 0, 0.58, 1)` | Fast start, slow end | +| `ease-in-out` | `cubic-bezier(0.42, 0, 0.58, 1)` | Slow start and end | + +### Custom Cubic-Bezier + +Create custom easing curves with `cubic-bezier(x1, y1, x2, y2)`: + +```css +/* Bouncy overshoot effect */ +.element { + animation-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6); +} + +/* Smooth deceleration */ +.element { + animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1); +} +``` + +**Tool:** Use [cubic-bezier.com](https://cubic-bezier.com) to visualize and create custom curves. + +### Popular Easing Functions + +```css +/* Ease Out Quart - Great for enter animations */ +cubic-bezier(0.25, 1, 0.5, 1) + +/* Ease In Out Cubic - Smooth state changes */ +cubic-bezier(0.65, 0, 0.35, 1) + +/* Ease Out Back - Slight overshoot */ +cubic-bezier(0.34, 1.56, 0.64, 1) + +/* Ease In Out Back - Overshoot both ends */ +cubic-bezier(0.68, -0.6, 0.32, 1.6) +``` + +### Steps Function + +For frame-by-frame animations (like sprite sheets): + +```css +/* 6 discrete steps */ +.sprite { + animation: walk 1s steps(6) infinite; +} + +/* Step positions */ +steps(4, jump-start) /* Jump at start of each interval */ +steps(4, jump-end) /* Jump at end of each interval (default) */ +steps(4, jump-both) /* Jump at both ends */ +steps(4, jump-none) /* No jump at ends */ +``` + +### Timing Function Per Keyframe + +Apply different easing to different segments: + +```css +@keyframes complexMove { + 0% { + transform: translateX(0); + animation-timing-function: ease-out; + } + 50% { + transform: translateX(100px); + animation-timing-function: ease-in; + } + 100% { + transform: translateX(200px); + } +} +``` + +**Important:** The timing function applies to each step individually, not the entire animation. + +--- + +## Fill Modes + +Fill modes control what styles apply before and after the animation runs. + +### Values + +| Value | Before Animation | After Animation | +|-------|-----------------|-----------------| +| `none` | Original styles | Original styles | +| `forwards` | Original styles | **Last keyframe styles** | +| `backwards` | **First keyframe styles** | Original styles | +| `both` | **First keyframe styles** | **Last keyframe styles** | + +### Common Problem: Element Snaps Back + +```css +/* BAD: Element disappears then reappears after animation */ +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +.element { + animation: fadeOut 1s; /* Element snaps back to opacity: 1 */ +} + +/* GOOD: Element stays invisible */ +.element { + animation: fadeOut 1s forwards; +} +``` + +### Backwards Fill Mode (for delays) + +```css +@keyframes slideIn { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Without backwards: element visible at original position during delay */ +/* With backwards: element starts at first keyframe position during delay */ +.element { + animation: slideIn 0.5s ease-out 1s backwards; +} +``` + +--- + +## Advanced Techniques + +### Animation Direction + +Control playback direction: + +```css +animation-direction: normal; /* 0% → 100% */ +animation-direction: reverse; /* 100% → 0% */ +animation-direction: alternate; /* 0% → 100% → 0% */ +animation-direction: alternate-reverse; /* 100% → 0% → 100% */ +``` + +**Use Case:** Breathing/pulsing effects + +```css +@keyframes breathe { + from { transform: scale(1); } + to { transform: scale(1.1); } +} + +.element { + animation: breathe 2s ease-in-out infinite alternate; +} +``` + +### Staggered Animations + +Create cascading effects with animation-delay: + +```css +.item { animation: fadeSlideIn 0.5s ease-out backwards; } +.item:nth-child(1) { animation-delay: 0ms; } +.item:nth-child(2) { animation-delay: 100ms; } +.item:nth-child(3) { animation-delay: 200ms; } +.item:nth-child(4) { animation-delay: 300ms; } + +/* Or use CSS custom properties */ +.item { + animation: fadeSlideIn 0.5s ease-out backwards; + animation-delay: calc(var(--i, 0) * 100ms); +} +``` + +In your template: + +```html +
First
+
Second
+
Third
+``` + +### @starting-style (Modern CSS) + +Define styles for when an element first enters the DOM: + +```css +.modal { + opacity: 1; + transform: scale(1); + transition: opacity 0.3s, transform 0.3s; + + @starting-style { + opacity: 0; + transform: scale(0.9); + } +} +``` + +### Animating Auto Height + +Use CSS Grid for height: auto animations: + +```css +.accordion-content { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.3s ease-out; +} + +.accordion-content.open { + grid-template-rows: 1fr; +} + +.accordion-content > div { + overflow: hidden; +} +``` + +### Pause/Play with CSS + +```css +.element { + animation: spin 2s linear infinite; + animation-play-state: running; +} + +.element:hover { + animation-play-state: paused; +} + +/* Or with a class */ +.element.paused { + animation-play-state: paused; +} +``` + +--- + +## Angular 20+ Integration + +### Using animate.enter and animate.leave + +Angular 20.2+ provides `animate.enter` and `animate.leave` to apply CSS classes when elements enter/leave the DOM. + +```typescript +@Component({ + selector: 'app-example', + template: ` + @if (isVisible()) { +
+ Content here +
+ } + + `, + styles: [` + .fade-in { + animation: fadeIn 0.3s ease-out; + } + + .fade-out { + animation: fadeOut 0.3s ease-in; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } + } + `] +}) +export class ExampleComponent { + isVisible = signal(false); + toggle() { this.isVisible.update(v => !v); } +} +``` + +### Dynamic Animation Classes + +```typescript +@Component({ + template: ` + @if (show()) { +
+ Dynamic animations! +
+ } + ` +}) +export class DynamicAnimComponent { + show = signal(false); + enterAnimation = signal('slide-in-right'); + leaveAnimation = signal('slide-out-left'); +} +``` + +### Reusable Animation CSS File + +Create a shared `animations.css`: + +```css +/* animations.css */ + +/* Fade animations */ +.fade-in { animation: fadeIn 0.3s ease-out; } +.fade-out { animation: fadeOut 0.3s ease-in; } + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +/* Slide animations */ +.slide-in-up { animation: slideInUp 0.3s ease-out; } +.slide-out-down { animation: slideOutDown 0.3s ease-in; } + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideOutDown { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +} + +/* Scale animations */ +.scale-in { animation: scaleIn 0.2s ease-out; } +.scale-out { animation: scaleOut 0.2s ease-in; } + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.9); + } +} +``` + +Import in `styles.css` or `angular.json`: + +```css +@import 'animations.css'; +``` + +--- + +## Common Patterns & Recipes + +### Loading Spinner + +```css +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid #f3f3f3; + border-top: 3px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} +``` + +### Skeleton Loading + +```css +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton { + background: linear-gradient( + 90deg, + #f0f0f0 25%, + #e0e0e0 50%, + #f0f0f0 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} +``` + +### Attention Pulse + +```css +@keyframes attention-pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5); + } + 50% { + box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); + } +} + +.notification-badge { + animation: attention-pulse 2s ease-in-out infinite; +} +``` + +### Shake (Error Feedback) + +```css +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(5px); } +} + +.error-input { + animation: shake 0.5s ease-in-out; +} +``` + +### Slide Down Menu + +```css +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdown-menu { + animation: slideDown 0.2s ease-out forwards; +} +``` + +### Toast Notification + +```css +@keyframes toastIn { + from { + opacity: 0; + transform: translateY(100%) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes toastOut { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(100%) scale(0.9); + } +} + +.toast { + animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.toast.leaving { + animation: toastOut 0.2s ease-in forwards; +} +``` + +--- + +## Performance Tips + +### Use Transform and Opacity + +These properties are GPU-accelerated and don't trigger layout: + +```css +/* GOOD - GPU accelerated */ +@keyframes good { + from { transform: translateX(0); opacity: 0; } + to { transform: translateX(100px); opacity: 1; } +} + +/* AVOID - Triggers layout recalculation */ +@keyframes avoid { + from { left: 0; width: 100px; } + to { left: 100px; width: 200px; } +} +``` + +### Use will-change Sparingly + +```css +.element { + will-change: transform, opacity; +} + +/* Remove after animation */ +.element.animation-complete { + will-change: auto; +} +``` + +### Respect Reduced Motion + +```css +@keyframes fadeSlide { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.element { + animation: fadeSlide 0.3s ease-out; +} + +@media (prefers-reduced-motion: reduce) { + .element { + animation: none; + /* Or use a simpler fade */ + animation: fadeIn 0.1s ease-out; + } +} +``` + +### Avoid Animating Layout Properties + +Properties that trigger layout (reflow): + +- `width`, `height` +- `top`, `left`, `right`, `bottom` +- `margin`, `padding` +- `font-size` +- `border-width` + +Use `transform: scale()` instead of `width/height` when possible. + +--- + +## Debugging Animations + +### Browser DevTools + +1. **Chrome DevTools** → More Tools → Animations + - Pause, slow down, or step through animations + - Inspect timing curves + +2. **Firefox** → Inspector → Animations tab + - Visual timeline of all animations + +### Force Slow Motion + +```css +/* Temporarily add to debug */ +* { + animation-duration: 3s !important; +} +``` + +### Animation Events in JavaScript + +```typescript +element.addEventListener('animationstart', (e) => { + console.log('Started:', e.animationName); +}); + +element.addEventListener('animationend', (e) => { + console.log('Ended:', e.animationName); + // Clean up class, remove element, etc. +}); + +element.addEventListener('animationiteration', (e) => { + console.log('Iteration:', e.animationName); +}); +``` + +### Common Issues + +| Problem | Solution | +|---------|----------| +| Animation not running | Check `animation-duration` is > 0 | +| Element snaps back | Add `animation-fill-mode: forwards` | +| Animation starts wrong | Use `animation-fill-mode: backwards` with delay | +| Choppy animation | Use `transform` instead of layout properties | +| Animation restarts on state change | Ensure Angular doesn't recreate the element | + +--- + +## Quick Reference Card + +```css +/* Basic setup */ +@keyframes name { + from { /* start */ } + to { /* end */ } +} + +.element { + animation: name 0.3s ease-out forwards; +} + +/* Angular 20+ */ +
+ +/* Shorthand order */ +animation: name duration timing delay count direction fill-mode state; + +/* Common timing functions */ +ease-out: cubic-bezier(0, 0, 0.58, 1) /* Enter animations */ +ease-in: cubic-bezier(0.42, 0, 1, 1) /* Exit animations */ +ease-in-out: cubic-bezier(0.42, 0, 0.58, 1) /* State changes */ + +/* Fill modes */ +forwards → Keep end state +backwards → Apply start state during delay +both → Both of the above +``` + +--- + +## Resources + +- [MDN CSS Animations Guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations) +- [Angular Animation Migration Guide](https://angular.dev/guide/animations/migration) +- [Cubic Bezier Tool](https://cubic-bezier.com) +- [Easing Functions Cheat Sheet](https://easings.net) +- [Josh W. Comeau's Keyframe Guide](https://www.joshwcomeau.com/animation/keyframe-animations/)