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 transform and opacity?
  • [ ] prefers-reduced-motion handled?
  • [ ] Using ease-out for user-initiated animations?
  • [ ] will-change applied only where needed?
  • [ ] Animation duration under 300ms for UI feedback (hover, click)?
  • [ ] Looping animations (spinners) using linear timing?
  • [ ] 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.