CSS Animations and Transitions
1. CSS Transition Properties (transition, transition-duration)
| Property | Values | Description | Default |
|---|---|---|---|
| transition-property | all | none | property-name |
Which properties to transition | all |
| transition-duration | time (s, ms) |
How long transition takes | 0s |
| transition-timing-function | ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() |
Speed curve of transition | ease |
| transition-delay | time (s, ms) |
Wait before starting transition | 0s |
| transition | property duration timing delay |
Shorthand for all transition properties | all 0s ease 0s |
| Timing Function | Curve | Description | Use Case |
|---|---|---|---|
| linear | Constant speed | Same speed start to finish | Progress bars, mechanical |
| ease | Slow-fast-slow | Accelerate then decelerate | General purpose (default) |
| ease-in | Slow start | Accelerate from start | Elements leaving screen |
| ease-out | Slow end | Decelerate to stop | Elements entering screen |
| ease-in-out | Slow-fast-slow | Smooth acceleration/deceleration | Smooth movement |
| cubic-bezier() | Custom curve | Full control over acceleration | Custom easing effects |
| steps() | Stepped animation | Jump between states | Sprite animations, typewriter |
Example: Basic transitions
/* Simple hover transition */
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: darkblue;
}
/* Multiple properties */
.card {
transform: scale(1);
opacity: 1;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.card:hover {
transform: scale(1.05);
opacity: 0.9;
}
/* Shorthand - all properties */
.box {
transition: all 0.3s ease;
}
/* Different timings for different properties */
.element {
transition:
transform 0.3s ease-out,
opacity 0.5s ease-in,
background-color 0.2s linear;
}
/* Transition with delay */
.delayed {
transition: opacity 0.5s ease 0.2s;
/* opacity, 0.5s duration, ease timing, 0.2s delay */
}
/* Specific properties only */
.specific {
transition-property: transform, opacity;
transition-duration: 0.3s;
transition-timing-function: ease-out;
}
Example: Advanced transition patterns
/* Staggered transitions */
.menu-item {
opacity: 0;
transform: translateX(-20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.menu-item:nth-child(1) { transition-delay: 0.1s; }
.menu-item:nth-child(2) { transition-delay: 0.2s; }
.menu-item:nth-child(3) { transition-delay: 0.3s; }
.menu.open .menu-item {
opacity: 1;
transform: translateX(0);
}
/* Bi-directional transitions (different in/out) */
.modal {
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
}
.modal.open {
opacity: 1;
transform: scale(1);
/* Fast opening: ease-out */
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
}
/* When closing, slower with ease-in */
.modal.closing {
transition: opacity 0.4s ease-in, transform 0.4s ease-in;
}
/* Custom cubic-bezier easing */
.bouncy {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Bouncy overshoot effect */
}
/* Material Design easing */
.material-standard {
transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.material-decelerate {
transition: all 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.material-accelerate {
transition: all 0.3s cubic-bezier(0.4, 0.0, 1, 1);
}
/* Stepped animation (sprite sheet) */
.sprite {
background: url('sprite.png');
width: 50px;
height: 50px;
transition: background-position 1s steps(10);
/* 10 frames */
}
.sprite:hover {
background-position: -500px 0; /* 10 frames * 50px */
}
Note: Only animatable properties can transition (transform,
opacity, colors, etc.). Avoid
transition: all in production (performance). Use specific properties
instead.
2. CSS Animation (@keyframes) and Animation Syntax
| Property | Values | Description | Default |
|---|---|---|---|
| animation-name | keyframe-name |
Name of @keyframes to use | none |
| animation-duration | time (s, ms) |
How long animation takes | 0s |
| animation-timing-function | ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() |
Speed curve of animation | ease |
| animation-delay | time (s, ms) |
Wait before starting | 0s |
| animation-iteration-count | number | infinite |
How many times to play | 1 |
| animation-direction | normal | reverse | alternate | alternate-reverse |
Play direction | normal |
| animation-fill-mode | none | forwards | backwards | both |
Styles before/after animation | none |
| animation-play-state | running | paused |
Pause or resume animation | running |
| animation | name duration timing delay count direction fill |
Shorthand for all animation properties | - |
| Keyframe Syntax | Description | Example |
|---|---|---|
| @keyframes | Define animation sequence | @keyframes slide { ... } |
| from/to | Simple 2-state animation | from { } to { } |
| Percentage | Multi-state with percentages | 0% { } 50% { } 100% { } |
| Multiple selectors | Same styles at different points | 0%, 100% { } |
Example: Basic keyframe animations
/* Simple fade in */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-element {
animation: fadeIn 1s ease;
}
/* Multi-step animation with percentages */
@keyframes bounce {
0% {
transform: translateY(0);
}
25% {
transform: translateY(-30px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
.bounce-element {
animation: bounce 1s ease;
}
/* Infinite spinning loader */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loader {
animation: spin 1s linear infinite;
}
/* Pulse animation */
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.pulse-button {
animation: pulse 2s ease-in-out infinite;
}
/* Slide in from left */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideInLeft 0.5s ease-out;
}
Example: Advanced animation patterns
/* Animation with fill-mode */
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.slide-up {
/* Keep final state after animation */
animation: slideUp 0.5s ease-out forwards;
}
/* Alternating direction */
@keyframes sway {
0%, 100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
.swaying {
animation: sway 3s ease-in-out infinite alternate;
/* Plays forward then backward */
}
/* Multiple animations */
.multi-animated {
animation:
fadeIn 1s ease-out,
slideUp 1s ease-out,
pulse 2s ease-in-out 1s infinite;
/* Third animation starts after 1s delay */
}
/* Paused animation (controlled by JS or hover) */
@keyframes scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
.scrolling-text {
animation: scroll 10s linear infinite;
}
.scrolling-text:hover {
animation-play-state: paused;
}
/* Complex multi-property animation */
@keyframes complexEntry {
0% {
transform: translateY(50px) scale(0.8) rotate(-5deg);
opacity: 0;
filter: blur(10px);
}
50% {
transform: translateY(-10px) scale(1.05) rotate(2deg);
filter: blur(0px);
}
100% {
transform: translateY(0) scale(1) rotate(0deg);
opacity: 1;
filter: blur(0px);
}
}
.complex-entry {
animation: complexEntry 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Typing effect with steps */
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink {
50% {
border-color: transparent;
}
}
.typewriter {
overflow: hidden;
border-right: 2px solid;
white-space: nowrap;
animation:
typing 3.5s steps(40) 1s forwards,
blink 0.75s step-end infinite;
}
Note: Use
animation-fill-mode: forwards to keep final state. infinite
loops should be performance-optimized. Use @keyframes for complex, multi-step
animations.
3. Animation Timing Functions (ease, ease-in, ease-out)
| Easing Function | cubic-bezier() Equivalent | Characteristic | Best For |
|---|---|---|---|
| linear | cubic-bezier(0, 0, 1, 1) |
Constant speed | Mechanical, loading bars |
| ease | cubic-bezier(0.25, 0.1, 0.25, 1) |
Slow start, fast middle, slow end | General purpose |
| ease-in | cubic-bezier(0.42, 0, 1, 1) |
Slow start, fast end | Exits, dismissals |
| ease-out | cubic-bezier(0, 0, 0.58, 1) |
Fast start, slow end | Entrances, reveals |
| ease-in-out | cubic-bezier(0.42, 0, 0.58, 1) |
Slow start and end | Smooth loops |
| Custom Easing | Values | Effect | Use Case |
|---|---|---|---|
| Bounce | cubic-bezier(0.68, -0.55, 0.265, 1.55) |
Overshoots and bounces back | Playful interactions |
| Elastic | cubic-bezier(0.68, -0.75, 0.265, 1.75) |
More extreme bounce | Attention-grabbing |
| Back | cubic-bezier(0.6, -0.28, 0.735, 0.045) |
Pull back before going forward | Anticipation effect |
| Material Standard | cubic-bezier(0.4, 0.0, 0.2, 1) |
Material Design standard | Android-style apps |
| iOS Swift Out | cubic-bezier(0.4, 0.0, 0, 1) |
iOS-style deceleration | iOS-style apps |
| steps() | steps(n, start|end) |
Jump between n discrete states | Sprite animations, flip counters |
Example: Custom easing functions
/* Bouncy button press */
.bouncy-button {
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.bouncy-button:active {
transform: scale(0.9);
}
/* Material Design motion */
.material-card {
transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
/* Elastic menu */
@keyframes elasticSlide {
0% {
transform: translateX(-100%);
}
60% {
transform: translateX(10%);
}
75% {
transform: translateX(-5%);
}
90% {
transform: translateX(2%);
}
100% {
transform: translateX(0);
}
}
.elastic-menu {
animation: elasticSlide 0.8s;
}
/* Or with cubic-bezier */
.elastic-simple {
animation: slideIn 0.8s cubic-bezier(0.68, -0.75, 0.265, 1.75);
}
/* Stepped animation for sprite */
@keyframes walk {
to {
background-position: -480px; /* 8 frames * 60px */
}
}
.character-walk {
width: 60px;
height: 60px;
background: url('walk-sprite.png');
animation: walk 0.8s steps(8) infinite;
}
/* Counter flip animation */
@keyframes flip {
to {
transform: translateY(-100%);
}
}
.flip-counter {
animation: flip 0.5s steps(10);
}
/* Smooth natural easing (iOS-style) */
.ios-sheet {
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
}
/* Anticipation effect (pull back before forward) */
@keyframes anticipate {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-20px);
}
100% {
transform: translateX(100%);
}
}
.anticipate-exit {
animation: anticipate 0.6s cubic-bezier(0.6, -0.28, 0.735, 0.045);
}
Example: Easing comparison and best practices
/* Entry animations: use ease-out (fast start, slow end) */
.enter {
animation: fadeIn 0.3s ease-out;
}
/* Exit animations: use ease-in (slow start, fast end) */
.exit {
animation: fadeOut 0.3s ease-in;
}
/* Movement: use ease-in-out */
.move {
transition: transform 0.5s ease-in-out;
}
/* Quick feedback: use shorter durations */
.button-press {
transition: transform 0.1s ease-out;
}
/* Attention-grabbing: use bounce */
.notification {
animation: bounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* Smooth infinite loop: use ease-in-out */
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
.floating {
animation: float 3s ease-in-out infinite;
}
/* Custom spring animation */
.spring {
animation: springIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* Stepped loading animation */
@keyframes loadingSteps {
to {
background-position: -200px 0;
}
}
.loading-bar {
animation: loadingSteps 1s steps(10) infinite;
}
/* Easing visualizer helper classes */
.easing-linear { animation-timing-function: linear; }
.easing-ease { animation-timing-function: ease; }
.easing-ease-in { animation-timing-function: ease-in; }
.easing-ease-out { animation-timing-function: ease-out; }
.easing-ease-in-out { animation-timing-function: ease-in-out; }
.easing-bounce { animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
Note: Use ease-out for entrances, ease-in for exits, ease-in-out for movement. Tools
like cubic-bezier.com help create custom curves. Material Design:
cubic-bezier(0.4, 0.0, 0.2, 1).
4. Scroll-triggered Animations Experimental
| Feature | Syntax | Description | Browser Support |
|---|---|---|---|
| animation-timeline | scroll() | view() |
Link animation to scroll position | Chrome 115+ |
| scroll() | scroll(axis container) |
Scroll container timeline | Limited |
| view() | view(axis inset) |
Element in viewport timeline | Limited |
| animation-range | start-point end-point |
When animation plays during scroll | Chrome 115+ |
| animation-range-start | cover 0% | contain 25% |
When animation starts | Chrome 115+ |
| animation-range-end | cover 100% | contain 75% |
When animation ends | Chrome 115+ |
Example: Scroll-driven animations (modern CSS)
/* Fade in as element enters viewport */
@keyframes fadeInScroll {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
animation: fadeInScroll linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* Parallax effect with scroll */
@keyframes parallax {
to {
transform: translateY(-100px);
}
}
.parallax-bg {
animation: parallax linear;
animation-timeline: scroll();
}
/* Scale based on scroll position */
@keyframes scaleOnScroll {
from {
transform: scale(0.8);
}
to {
transform: scale(1);
}
}
.scale-scroll {
animation: scaleOnScroll linear;
animation-timeline: view();
animation-range: entry 0% cover 50%;
}
/* Header shrink on scroll */
@keyframes shrinkHeader {
to {
transform: scale(0.9);
padding: 0.5rem 1rem;
}
}
.header {
animation: shrinkHeader linear;
animation-timeline: scroll(root block);
animation-range: 0 100px;
}
/* Rotate on scroll */
@keyframes rotateScroll {
to {
transform: rotate(360deg);
}
}
.rotate-element {
animation: rotateScroll linear;
animation-timeline: scroll();
}
/* Progress bar based on scroll */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: blue;
transform-origin: 0 50%;
animation: scaleProgress linear;
animation-timeline: scroll(root block);
}
@keyframes scaleProgress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
Example: Scroll animations with Intersection Observer (fallback)
/* CSS animation classes */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
opacity: 0;
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.active {
animation: fadeInUp 0.6s ease forwards;
}
/* JavaScript (Intersection Observer) */
/*
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
});
document.querySelectorAll('.reveal').forEach(el => {
observer.observe(el);
});
*/
/* Staggered scroll reveals */
.reveal-stagger {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease;
}
.reveal-stagger.active {
opacity: 1;
transform: translateY(0);
}
.reveal-stagger:nth-child(1) { transition-delay: 0.1s; }
.reveal-stagger:nth-child(2) { transition-delay: 0.2s; }
.reveal-stagger:nth-child(3) { transition-delay: 0.3s; }
/* Parallax sections */
.parallax-section {
transform: translateY(calc(var(--scroll-progress) * -50px));
transition: transform 0.1s linear;
}
/* Scroll-linked opacity */
.fade-on-scroll {
opacity: calc(1 - var(--scroll-progress));
}
/* Pin element during scroll (with scroll-timeline) */
@keyframes pinSticky {
0% {
transform: translateY(0);
}
100% {
transform: translateY(0);
}
}
.pin-element {
position: sticky;
top: 0;
animation: pinSticky linear;
animation-timeline: view();
}
Warning:
animation-timeline is experimental (Chrome
115+). Use Intersection Observer for production. Performance: limit scroll animations, use
will-change, prefer transform/opacity.
5. View Transition API Integration Experimental
| API Method | Syntax | Description | Browser Support |
|---|---|---|---|
| document.startViewTransition() | startViewTransition(callback) |
Create animated transition between states | Chrome 111+ |
| view-transition-name | unique-name |
Identify element for transition | Chrome 111+ |
| ::view-transition | Pseudo-element | Root transition container | Chrome 111+ |
| ::view-transition-group() | Pseudo-element | Group for specific transition | Chrome 111+ |
| ::view-transition-image-pair() | Pseudo-element | Old and new image container | Chrome 111+ |
| ::view-transition-old() | Pseudo-element | Outgoing state snapshot | Chrome 111+ |
| ::view-transition-new() | Pseudo-element | Incoming state snapshot | Chrome 111+ |
Example: View Transition API basics
/* Mark elements for view transitions */
.card {
view-transition-name: card-1;
}
.title {
view-transition-name: title;
}
/* Customize transition animation */
::view-transition-old(card-1),
::view-transition-new(card-1) {
animation-duration: 0.5s;
}
/* Custom animation for specific transition */
@keyframes slide-from-right {
from {
transform: translateX(100%);
}
}
::view-transition-new(card-1) {
animation: slide-from-right 0.5s ease-out;
}
/* Fade transition */
::view-transition-old(root) {
animation: 0.3s ease-out both fade-out;
}
::view-transition-new(root) {
animation: 0.3s ease-out both fade-in;
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
/* JavaScript usage */
/*
// Simple transition
function updateView() {
document.startViewTransition(() => {
// Update DOM here
document.querySelector('.content').textContent = 'New content';
});
}
// With promise handling
async function animatedUpdate() {
const transition = document.startViewTransition(() => {
// DOM updates
updateDOMState();
});
await transition.finished;
console.log('Transition complete');
}
// Conditional transitions
function conditionalTransition() {
if (document.startViewTransition) {
document.startViewTransition(() => updateDOM());
} else {
updateDOM(); // Fallback without animation
}
}
*/
Example: Advanced View Transition patterns
/* Different transitions for different elements */
.hero {
view-transition-name: hero;
}
.sidebar {
view-transition-name: sidebar;
}
/* Hero expands */
::view-transition-new(hero) {
animation: scaleUp 0.5s ease-out;
}
@keyframes scaleUp {
from {
transform: scale(0.8);
}
}
/* Sidebar slides in */
::view-transition-new(sidebar) {
animation: slideInLeft 0.4s ease-out;
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
}
/* Morph between layouts */
.grid-item {
view-transition-name: attr(data-id); /* Dynamic names */
}
/* Smooth color transitions */
::view-transition-group(root) {
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
}
/* Page transition (SPA navigation) */
::view-transition-old(root) {
animation: fadeOutScale 0.3s ease-in;
}
::view-transition-new(root) {
animation: fadeInScale 0.3s ease-out;
}
@keyframes fadeOutScale {
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(1.05);
}
}
/* Disable transition for specific elements */
.no-transition {
view-transition-name: none;
}
/* Cross-fade between images */
.image-container {
view-transition-name: main-image;
}
::view-transition-old(main-image),
::view-transition-new(main-image) {
animation-duration: 0.4s;
height: 100%;
object-fit: cover;
}
/* Custom easing for smooth transitions */
::view-transition-group(*) {
animation-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1);
}
Warning: View Transitions API is experimental (Chrome 111+). Not
yet in Firefox/Safari. Always provide fallback. Only works for same-document transitions. Feature detection
required:
if (document.startViewTransition) { }
6. Performance-optimized Animations (will-change)
| Optimization | Technique | Benefit | When to Use |
|---|---|---|---|
| GPU Properties | Animate transform and opacity only |
60fps performance, no layout/paint | All animations |
| will-change | will-change: transform |
Prepare GPU layer in advance | Before animation starts |
| contain | contain: layout paint |
Isolate animation effects | Complex animated sections |
| Reduce complexity | Limit animated elements | Less work per frame | Many simultaneous animations |
| requestAnimationFrame | JS animations synced to browser | Smooth frame timing | JavaScript-based animations |
| Debounce scroll | Throttle scroll event handlers | Reduce event handler calls | Scroll-triggered animations |
| Property | Performance Impact | Triggers | Recommendation |
|---|---|---|---|
| transform | Excellent | Composite only | ✓ Use for movement |
| opacity | Excellent | Composite only | ✓ Use for fades |
| filter | Good | Paint + Composite | ⚠ Use sparingly |
| width/height | Poor | Layout + Paint + Composite | ✗ Avoid animating |
| top/left/right/bottom | Poor | Layout + Paint + Composite | ✗ Use transform instead |
| margin/padding | Poor | Layout + Paint + Composite | ✗ Use transform instead |
| background-color | Moderate | Paint + Composite | ⚠ Short durations only |
Example: Performance-optimized animations
/* GOOD: GPU-accelerated */
.optimized {
transition: transform 0.3s, opacity 0.3s;
}
.optimized:hover {
transform: translateY(-5px);
opacity: 0.8;
}
/* BAD: Triggers layout */
.bad {
transition: top 0.3s, height 0.3s;
}
.bad:hover {
top: -5px;
height: 150px;
}
/* will-change optimization */
.about-to-animate {
will-change: transform, opacity;
}
.about-to-animate.animating {
animation: slideIn 0.5s;
}
/* Remove will-change after animation */
.about-to-animate.done {
will-change: auto;
}
/* Contain for isolated animations */
.animated-card {
contain: layout paint;
/* Animation won't affect outside elements */
}
/* Efficient loader */
.loader {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
/* Only animates transform - GPU accelerated */
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Efficient pulse effect */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
}
/* Reduce animation on low-end devices */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Efficient stagger without JavaScript */
.stagger-item {
animation: fadeIn 0.5s ease-out backwards;
}
.stagger-item:nth-child(1) { animation-delay: 0.1s; }
.stagger-item:nth-child(2) { animation-delay: 0.2s; }
.stagger-item:nth-child(3) { animation-delay: 0.3s; }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
}
Example: Performance best practices
/* Layer promotion for complex animations */
.complex-animation {
transform: translateZ(0); /* Force GPU layer */
will-change: transform;
}
/* Clean up will-change after animation */
/*
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
*/
/* Efficient scroll animations */
.scroll-animate {
transition: transform 0.1s ease-out;
/* Short duration for responsiveness */
}
/* Throttle expensive updates */
/*
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateAnimation();
ticking = false;
});
ticking = true;
}
});
*/
/* Reduce animation complexity for mobile */
@media (max-width: 768px) {
.complex-animation {
animation: simpleAnimation 0.3s ease;
}
}
/* Pause animations when not visible */
/*
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.animationPlayState = 'running';
} else {
entry.target.style.animationPlayState = 'paused';
}
});
});
*/
/* Efficient color transitions */
.color-change {
/* Background-color is paint-only, relatively cheap */
transition: background-color 0.2s ease;
}
/* Avoid filter animations on mobile */
@media (hover: none) {
.filtered {
/* Remove expensive filter animations on touch devices */
filter: none;
}
}
/* Use animation instead of transition for loops */
.infinite-loop {
/* Animation is more efficient than transitioning back and forth */
animation: rotate 2s linear infinite;
}
@keyframes rotate {
to { transform: rotate(360deg); }
}
/* Batch DOM reads and writes */
/*
// BAD: Causes layout thrashing
elements.forEach(el => {
const height = el.offsetHeight; // Read
el.style.height = height * 2 + 'px'; // Write
});
// GOOD: Batch reads, then writes
const heights = elements.map(el => el.offsetHeight); // All reads
heights.forEach((height, i) => {
elements[i].style.height = height * 2 + 'px'; // All writes
});
*/
Animation & Transition Best Practices
- Only animate transform and opacity for 60fps performance
- Use
transitionfor state changes,animationfor complex sequences - Use ease-out for entrances, ease-in for exits, ease-in-out for movement
- Apply
will-changetemporarily before animations, remove after - Use
animation-fill-mode: forwardsto keep final state - Respect
prefers-reduced-motionfor accessibility - Keep durations 200-500ms for UI interactions, 1-2s for attention-grabbing
- Use
steps()for sprite animations,cubic-bezier()for custom easing - Test animations on low-end devices, provide simpler fallbacks