mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ feat: add css-keyframes-animations skill for native CSS animations
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
This commit is contained in:
392
.claude/skills/css-keyframes-animations/SKILL.md
Normal file
392
.claude/skills/css-keyframes-animations/SKILL.md
Normal file
@@ -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()) {
|
||||
<div animate.enter="fade-in" animate.leave="fade-out">
|
||||
Content
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### 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) {
|
||||
<div [style.--i]="idx" class="stagger-item">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
**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()) {
|
||||
<div animate.enter="fade-in" animate.leave="fade-out">
|
||||
Content
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
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()) {
|
||||
<div [animate.enter]="enterAnim()" [animate.leave]="leaveAnim()">
|
||||
Content
|
||||
</div>
|
||||
}
|
||||
`
|
||||
})
|
||||
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
|
||||
<div animate.enter="fade-in" animate.leave="slide-out-down">
|
||||
```
|
||||
|
||||
## 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()) {
|
||||
<div animate.enter="fade-in">Content</div>
|
||||
}
|
||||
`,
|
||||
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
|
||||
278
.claude/skills/css-keyframes-animations/assets/animations.css
Normal file
278
.claude/skills/css-keyframes-animations/assets/animations.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
<div class="item" style="--i: 0">First</div>
|
||||
<div class="item" style="--i: 1">Second</div>
|
||||
<div class="item" style="--i: 2">Third</div>
|
||||
```
|
||||
|
||||
### @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()) {
|
||||
<div animate.enter="fade-in" animate.leave="fade-out">
|
||||
Content here
|
||||
</div>
|
||||
}
|
||||
<button (click)="toggle()">Toggle</button>
|
||||
`,
|
||||
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()) {
|
||||
<div [animate.enter]="enterAnimation()" [animate.leave]="leaveAnimation()">
|
||||
Dynamic animations!
|
||||
</div>
|
||||
}
|
||||
`
|
||||
})
|
||||
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+ */
|
||||
<div animate.enter="fade-in" animate.leave="fade-out">
|
||||
|
||||
/* 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/)
|
||||
Reference in New Issue
Block a user