Transitions and Animations
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 function | Feels like | Use for |
|---|---|---|
ease | quick start, soft landing | The sane default |
ease-out | fast, then settles | Elements entering the screen |
ease-in | hesitates, then goes | Elements leaving |
linear | mechanical, constant | Loops, 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
| Property | Role | Example |
|---|---|---|
animation-name | The @keyframes to run | fade-slide-in |
animation-duration | How long one cycle runs | 260ms |
animation-timing-function | Speed curve per cycle | ease-out |
animation-iteration-count | How many cycles | 1 or infinite |
animation-fill-mode | Which keyframe states apply outside the run | backwards, forwards |
animation-play-state | Pause/resume a running animation | paused |
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.