Transitions and Animations

Motion is information: it shows where an element came from, what changed, and what belongs together. CSS gives you two mechanisms — transitions for state changes and keyframe animations for multi-step sequences. Use them deliberately: fast, subtle, and always removable by the user.

Transitions first

The recipe: property, duration, easing

A transition interpolates between an element's old and new value when a state change happens — hover, focus, a class toggle, a media query flipping. Declare the transition on the resting state so it applies in both directions:

/* Declare on the resting state: the transition then runs
   on hover IN and hover OUT alike. */
.btn-primary {
  background: #2563eb;
  transform: translateY(0);
  /* List properties explicitly - `all` also animates layout
     properties and makes future edits surprisingly expensive */
  transition: background-color 180ms ease, transform 180ms ease;
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: #1d4ed8;
  transform: translateY(-2px);
}

Easing and duration in plain terms

Timing functionFeels likeUse for
easequick start, soft landingThe sane default
ease-outfast, then settlesElements entering the screen
ease-inhesitates, then goesElements leaving
linearmechanical, constantLoops, progress, color cycles

Interface transitions sit roughly between 100ms (micro-feedback) and 300ms (larger surfaces). Below 100ms reads as a glitch; above 300–500ms users start waiting for the interface instead of using it. These are conventions from motion practice, not spec limits — calibrate against your own UI.

Animate only the cheap properties

The browser can interpolate transform and opacity on the compositor thread — they never re-run layout or painting. Animating width, top, or margin re-runs layout on every frame; on low-end devices that is visible jank. The standard reframe: move and scale with transform, fade with opacity.

/* Instead of animating height for a reveal, scale the space itself */
.reveal {
  transform-origin: top;
  transition: transform 200ms ease-out, opacity 200ms ease-out;
}
.reveal[hidden="until-found"] { transform: scaleY(0); opacity: 0; }

Keyframe animations

When keyframes, and not transitions

Transitions need a state change to trigger and know only two endpoints. Keyframes run autonomously (on load, on an interval, forever) and support any number of stops. Entry effects, loading indicators, and looping emphasis are keyframe territory:

/* Define the sequence once, reference it by name */
@keyframes fade-slide-in {
  from {
    opacity: 0;
    transform: translateY(12px);   /* starts slightly below... */
  }
  to {
    opacity: 1;
    transform: translateY(0);      /* ...and settles into place */
  }
}

.panel {
  animation: fade-slide-in 260ms ease-out backwards;
  /* `backwards` applies the `from` state during any delay,
     so a staggered list starts invisible, not stacked on top of each other */
}

/* Staggered entries: one keyframe, per-item delays */
.panel:nth-child(2) { animation-delay: 80ms; }
.panel:nth-child(3) { animation-delay: 160ms; }

The animation properties in one table

PropertyRoleExample
animation-nameThe @keyframes to runfade-slide-in
animation-durationHow long one cycle runs260ms
animation-timing-functionSpeed curve per cycleease-out
animation-iteration-countHow many cycles1 or infinite
animation-fill-modeWhich keyframe states apply outside the runbackwards, forwards
animation-play-statePause/resume a running animationpaused

Reduced motion is not optional

The one block every stylesheet should have

Users can ask their OS to minimize motion (vestibular disorders, migraines, distraction). The browser exposes that as a media query. The thorough version removes motion but not feedback — elements still change state, just without traveling:

/* Kill motion, keep the state change itself */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Practice: motion with manners

The task

Open demo/quality/18_animations.html from the Lab Examples page. Toggle your OS "reduce motion" setting (or the demo's checkbox) and watch which effects survive.

The checklist

  • Build a card that fades in on load and lifts on hover, both under 250ms.
  • Stagger three list items with one keyframe and per-item delays.
  • Re-create one layout animation (animating width) and one transform animation of the same idea; compare CPU usage in dev tools' Performance panel.
  • Add the reduced-motion block to a stylesheet you already wrote and confirm nothing breaks.