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
16 KiB
CSS @keyframes Deep Dive
A comprehensive guide for Angular developers transitioning from @angular/animations to native CSS animations.
Table of Contents
- Understanding @keyframes
- Basic Syntax
- Animation Properties
- Timing Functions (Easing)
- Fill Modes
- Advanced Techniques
- Angular 20+ Integration
- Common Patterns & Recipes
- Performance Tips
- 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
@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:
@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:
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
Applying the Animation
.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
/* 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
.element {
animation: fadeIn 1s;
}
Multiple Animations
Apply multiple animations to a single element:
.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):
/* 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 to visualize and create custom curves.
Popular Easing Functions
/* 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):
/* 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:
@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
/* 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)
@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:
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
@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:
.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:
<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:
.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:
.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
.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.
@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
@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:
/* 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:
@import 'animations.css';
Common Patterns & Recipes
Loading Spinner
@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
@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
@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)
@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
@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
@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:
/* 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
.element {
will-change: transform, opacity;
}
/* Remove after animation */
.element.animation-complete {
will-change: auto;
}
Respect Reduced Motion
@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,heighttop,left,right,bottommargin,paddingfont-sizeborder-width
Use transform: scale() instead of width/height when possible.
Debugging Animations
Browser DevTools
-
Chrome DevTools → More Tools → Animations
- Pause, slow down, or step through animations
- Inspect timing curves
-
Firefox → Inspector → Animations tab
- Visual timeline of all animations
Force Slow Motion
/* Temporarily add to debug */
* {
animation-duration: 3s !important;
}
Animation Events in JavaScript
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
/* 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