CSS animations and transitions are the tools for making interfaces feel responsive and polished without JavaScript. They're hardware-accelerated, performant, and declarative. Knowing when to use transitions vs animations — and how to make them not annoy users — is the practical skill.
Transitions
Transitions animate property changes between two states. They require a trigger (hover, focus, class change):
.button {
background: #2563eb;
transform: scale(1);
transition: background 200ms ease, transform 150ms ease;
}
.button:hover {
background: #1d4ed8;
transform: scale(1.03);
}
The transition shorthand: property duration easing delay.
Multiple Properties
/* Animate specific properties */
transition: opacity 200ms ease, transform 300ms ease-out;
/* Animate all changed properties — use sparingly */
transition: all 200ms ease;
Avoid all — it can catch unexpected property changes and create weird visual artifacts.
Timing Functions
The easing function controls the pace of the animation:
transition-timing-function: ease; /* slow start, fast middle, slow end */
transition-timing-function: ease-in; /* slow start, fast end */
transition-timing-function: ease-out; /* fast start, slow end */
transition-timing-function: ease-in-out; /* slow start and end */
transition-timing-function: linear; /* constant speed */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* custom bounce */
For UI elements: ease-out feels most natural (fast response, gradual settle). ease-in feels sluggish for user-initiated actions.
Animations
Animations play automatically (no trigger needed), can loop, and can animate through multiple keyframes:
@keyframes fade-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.notification {
animation: fade-in 250ms ease-out;
}
Multiple keyframes:
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.badge {
animation: pulse 2s ease-in-out infinite;
}
Animation shorthand: name duration easing delay iteration-count direction fill-mode.
.spinner {
animation: spin 1s linear infinite;
}
.toast {
animation: slide-in 200ms ease-out forwards;
/* 'forwards' keeps the final state after animation completes */
}
What to Animate (and What Not To)
Performant — animate these:
-
transform: translate,scale,rotate opacity
These are composited by the browser — handled on the GPU without triggering layout or paint. Smooth on most devices.
Avoid animating:
-
width,height,top,left,margin,padding -
border-width,font-size - Any property that triggers layout recalculation
These cause layout thrashing on every frame. Use transform: translate() instead of animating top/left.
/* Slow: triggers layout every frame */
.moving {
transition: left 300ms ease;
}
.moving:hover { left: 100px; }
/* Fast: GPU-composited */
.moving {
transition: transform 300ms ease;
}
.moving:hover { transform: translateX(100px); }
will-change
Hint to the browser that an element will be animated:
.card {
will-change: transform;
}
This tells the browser to promote the element to its own compositor layer in advance. Use sparingly — every will-change consumes GPU memory. Apply it only to elements you know will animate, not as a blanket optimization.
Respecting User Preferences
Some users disable animations for accessibility or motion sensitivity. Always respect prefers-reduced-motion:
.notification {
animation: slide-in 250ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.notification {
animation: none;
/* Or use a simple fade instead */
}
}
Or set animations to zero by default and only enable them when preferred:
/* Disable all animations globally unless user accepts motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Firefox respects the system's prefers-reduced-motion setting. Test with it enabled.
State-Based Animations with CSS Custom Properties
CSS custom properties let you drive animations dynamically from JavaScript:
.progress {
width: 0%;
transition: width 500ms ease-out;
}
document.querySelector('.progress').style.width = '75%';
// CSS transition handles the animation
Or with a custom property:
.bar {
transform: scaleX(var(--progress, 0));
transform-origin: left;
transition: transform 500ms ease-out;
}
document.querySelector('.bar').style.setProperty('--progress', 0.75);
Staggered Animations
Animate a list of items in sequence using animation-delay:
.list-item {
opacity: 0;
animation: fade-in 200ms ease-out forwards;
}
.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; }
Or dynamically with CSS custom properties:
<li style="--delay: 100ms">Item</li>
.list-item {
animation: fade-in 200ms ease-out var(--delay, 0ms) forwards;
}
Loading Skeletons
Skeleton loading states are a good use case for CSS animations:
.skeleton {
background: linear-gradient(
90deg,
#e5e7eb 0%,
#f3f4f6 50%,
#e5e7eb 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
Dark Mode Transitions
Avoid jarring color mode switches with a transition on color changes:
:root {
--bg: #ffffff;
--text: #111827;
}
:root.dark {
--bg: #111827;
--text: #f9fafb;
}
body {
background: var(--bg);
color: var(--text);
transition: background 200ms ease, color 200ms ease;
}
Performance Checklist
- [ ] Only animating
transformandopacity? - [ ]
prefers-reduced-motionhandled? - [ ] Using
ease-outfor user-initiated animations? - [ ]
will-changeapplied only where needed? - [ ] Animation duration under 300ms for UI feedback (hover, click)?
- [ ] Looping animations (spinners) using
lineartiming? - [ ] Not using
transition: all?
The most common mistake is animating layout-triggering properties instead of transforms. Switch any width/height/top/left animation to an equivalent transform, and you'll immediately see smoother performance on lower-end devices.
Top comments (0)