DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

CSS Animations and Transitions: What to Animate, prefers-reduced-motion, and Performance

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 */
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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); }
Enter fullscreen mode Exit fullscreen mode

will-change

Hint to the browser that an element will be animated:

.card {
  will-change: transform;
}
Enter fullscreen mode Exit fullscreen mode

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 */
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
document.querySelector('.progress').style.width = '75%';
// CSS transition handles the animation
Enter fullscreen mode Exit fullscreen mode

Or with a custom property:

.bar {
  transform: scaleX(var(--progress, 0));
  transform-origin: left;
  transition: transform 500ms ease-out;
}
Enter fullscreen mode Exit fullscreen mode
document.querySelector('.bar').style.setProperty('--progress', 0.75);
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

Or dynamically with CSS custom properties:

<li style="--delay: 100ms">Item</li>
Enter fullscreen mode Exit fullscreen mode
.list-item {
  animation: fade-in 200ms ease-out var(--delay, 0ms) forwards;
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)