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.
