CSS Animations and Transitions: Complete Guide with Examples
CSS animations and transitions bring UIs to life. Done well, they guide attention and improve perceived performance. Done poorly, they distract and slow down your app. Here's how to do them right.
CSS Transitions: Simple State Changes
Transitions animate property changes between states (hover, focus, active, class change).
/* Syntax: transition: property duration timing-function delay */
.button {
background: #3b82f6;
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Animate specific properties */
transition: background 200ms ease,
transform 150ms ease,
box-shadow 200ms ease;
}
.button:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
/* Shorthand: transition all properties */
.button { transition: all 200ms ease; }
/* ⚠️ Avoid transition: all in production — performance impact */
Timing Functions
.element {
transition-timing-function: ease; /* default: slow-fast-slow */
transition-timing-function: linear; /* constant speed */
transition-timing-function: ease-in; /* starts slow */
transition-timing-function: ease-out; /* ends slow ← use for UI elements entering */
transition-timing-function: ease-in-out; /* both ends slow */
/* Custom cubic-bezier */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring bounce */
/* Spring-like */
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
Good Transition Durations
Small UI feedback (hover, active): 100-150ms
Color/opacity changes: 200-300ms
Panels, drawers sliding in: 200-350ms
Page transitions: 300-500ms
Decorative/hero animations: 500ms-1s
Rule: Transitions that respond to user input should feel instant (< 200ms)
Decorative animations can be slower
CSS Animations: @keyframes
Animations run automatically, can loop, and have more control points.
/* Define the animation */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Or with percentages for multiple steps */
@keyframes slideInUp {
0% { transform: translateY(20px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Apply animation */
.card {
animation-name: fadeIn;
animation-duration: 300ms;
animation-timing-function: ease-out;
animation-delay: 0ms;
animation-iteration-count: 1; /* or: infinite, 3 */
animation-direction: normal; /* or: reverse, alternate */
animation-fill-mode: both; /* keep final state */
animation-play-state: running; /* or: paused */
/* Shorthand */
animation: fadeIn 300ms ease-out both;
/* Multiple animations */
animation: fadeIn 300ms ease-out, pulse 2s ease-in-out infinite 300ms;
}
animation-fill-mode (Important!)
/* none: element returns to original state after animation */
.element { animation: fadeIn 500ms none; }
/* forwards: keeps the final keyframe state */
.element { animation: fadeIn 500ms forwards; }
/* backwards: applies first keyframe during delay period */
.element { animation: fadeIn 500ms 200ms backwards; }
/* both: applies backwards and forwards */
.element { animation: fadeIn 500ms 200ms both; } /* ← usually what you want */
Practical Animation Patterns
Loading Skeleton
@keyframes shimmer {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-text { height: 1em; width: 80%; margin-bottom: 8px; }
.skeleton-title { height: 1.5em; width: 60%; }
Spinner
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(59, 130, 246, 0.2);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 600ms linear infinite;
}
Notification Toast Slide-In
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(110%);
opacity: 0;
}
}
.toast {
animation: slideInRight 250ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.toast.dismissing {
animation: slideOutRight 200ms ease-in both;
}
Staggered List Animation
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.list-item {
animation: fadeSlideIn 300ms ease-out both;
}
/* Stagger using nth-child */
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
.list-item:nth-child(4) { animation-delay: 150ms; }
/* Better: use CSS custom properties set via JS */
.list-item {
animation-delay: calc(var(--index, 0) * 50ms);
}
// Set --index on each item
document.querySelectorAll('.list-item').forEach((item, i) => {
item.style.setProperty('--index', i);
});
Performance: The Critical Rule
Only animate transform and opacity. These run on the GPU compositor thread without triggering layout.
/* ✅ GPU-accelerated: smooth 60fps */
.element {
transform: translateX(100px); /* move */
transform: scale(1.1); /* resize */
transform: rotate(45deg); /* rotate */
opacity: 0.5; /* fade */
}
/* ❌ Triggers layout (expensive): causes jank */
.element {
width: 200px; /* layout */
height: 100px; /* layout */
top: 20px; /* layout */
left: 10px; /* layout */
margin: 10px; /* layout */
}
/* ❌ Triggers paint (moderate cost) */
.element {
background-color: red; /* paint */
color: blue; /* paint */
box-shadow: ...; /* paint */
}
will-change
/* Hint to browser to create a new compositor layer */
.animated-element {
will-change: transform, opacity;
}
/* ⚠️ Use sparingly — each layer uses GPU memory */
/* Add before animation starts, remove after */
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
Accessibility: Respect User Preferences
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
/* Remove animations for users who prefer it */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Or provide alternative reduced animations */
@media (prefers-reduced-motion: no-preference) {
.hero-animation {
animation: fadeSlideIn 600ms ease-out both;
}
}
CSS Animation vs JavaScript Animation
| Feature | CSS | JS (GSAP, etc.) |
|---|---|---|
| Performance | ✅ Compositor-friendly | ✅ With proper use |
| Control | Limited | Full control |
| Sequencing | Hard | Easy |
| Physics | No | Yes (spring) |
| Interactivity | Limited | Full |
| SVG paths | Limited | Yes |
| Best for | Simple state changes | Complex timelines |
For most UI transitions → CSS. For complex sequences/scroll animations → GSAP or Web Animations API.
→ Explore color schemes for your animations with the Color Converter.