EduFlow
Modern CSS Mastery
0%

Section 4 · Motion and Architecture

Transitions, Keyframes and Reduced Motion

18 min3 min readCode playground

Lesson at a glance

Add transitions and keyframe animations that feel deliberate, animate elements in from display none with @starting-style, and honour prefers-reduced-motion without breaking the interface.

  1. 1Transitions: between two states
  2. 2Keyframes: multi-step and looping motion
  3. 3Entry animations with @starting-style
  4. 4Respect reduced motion
  5. 5Your turn

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Waypoint’s design brief asks for three bits of motion: session cards lift slightly on hover, the session that’s happening right now has a pulsing “Live” dot, and a toast slides in when you save a session. All three are small. All three can go wrong in ways that make the page feel cheap, or make some people physically unwell.

This lesson covers the tools (transitions, keyframes, @starting-style) and the responsibility (prefers-reduced-motion).

Transitions: between two states

A transition animates a property when its computed value changes, for whatever reason: hover, a class toggle, a media query. You choose which properties, how long, and the easing:

session-card.css
.session-card {
  transition:
    translate 200ms ease-out,
    box-shadow 200ms ease-out;

  &:hover,
  &:focus-within {
    translate: 0 -2px;
    box-shadow: 0 6px 16px rgb(15 23 42 / 0.12);
  }
}

Three choices are doing work here. Specific properties: the card lifts and its shadow grows, nothing else animates. Short duration: 150–250ms feels responsive for small UI changes; anything over 400ms feels sluggish when the user is waiting on it. Easing: ease-out starts fast and settles, which suits things responding to the user.

The individual transform properties (translate, scale, rotate) are worth using over transform: they can be transitioned and overridden independently, so a hover lift doesn’t wipe out a scale set somewhere else.

Keyframes: multi-step and looping motion

When motion has more than two steps, or repeats on its own, you need @keyframes:

CSS
@keyframes pulse {
  0%   { scale: 1;   opacity: 1; }
  70%  { scale: 2.2; opacity: 0; }
  100% { scale: 2.2; opacity: 0; }
}

.live-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: currentColor;
  animation: pulse 1.6s ease-out infinite;
}

The dot’s halo grows and fades, then pauses briefly (the 70%–100% hold) before repeating. Animating scale and opacity is deliberate: those two can run on the compositor without re-running layout, which the last lesson in this course explains.

Entry animations with @starting-style

The save toast is display: none until a class shows it. Transitions can’t run on its first appearance, because a newly rendered element has no previous style to start from. @starting-style provides one:

CSS
.toast {
  opacity: 1;
  translate: 0 0;
  transition:
    opacity 200ms ease-out,
    translate 200ms ease-out,
    display 200ms allow-discrete;
}

@starting-style {
  .toast {
    opacity: 0;
    translate: 0 8px;
  }
}

.toast[hidden] {
  display: none;
  opacity: 0;
}

When hidden is removed, the browser starts the toast at the @starting-style values and transitions to the normal ones. transition-behavior: allow-discrete (here written as allow-discrete in the shorthand) lets display flip at the right moment on the way out, so the fade-out is visible before the element disappears. @starting-style and transition-behavior are Baseline 2024, so the entry animation works everywhere current. Animating display itself on the way out isn’t Baseline yet (Chrome and Safari do it, Firefox doesn’t), so there the toast disappears instantly instead of fading; in older browsers it simply appears without animation. Both are perfectly good fallbacks.

Respect reduced motion

Some people get dizzy, nauseous or distracted from motion on screen, especially from movement across distance, parallax and zooming. Operating systems have a “reduce motion” setting, and CSS reads it with prefers-reduced-motion.

The most robust pattern makes motion opt-in:

CSS
@media (prefers-reduced-motion: no-preference) {
  .session-card {
    transition: translate 200ms ease-out, box-shadow 200ms ease-out;
  }
  .live-dot::after {
    animation: pulse 1.6s ease-out infinite;
  }
}

Users who haven’t asked for reduced motion get everything. Users who have get a static card and a static dot, and the interface still works, because no information depends on the motion.

You’ll also see a global safety net that sets every animation and transition duration to almost zero inside @media (prefers-reduced-motion: reduce). It’s a reasonable backstop for third-party widgets, but as your only strategy it’s blunt: it can confuse scripts that rely on animation timing (it uses a tiny non-zero duration precisely because a 0s transition never fires transitionend), and it removes harmless fades along with harmful motion.

Your turn

The exercise card has an instant hover, a static dot, and no reduced-motion handling. Add a hover transition on specific properties, a looping pulse on the dot, and wrap the decorative motion so it only runs for users who haven’t asked to reduce it. Next, you’ll animate between whole page states with view transitions.

Key takeaways

  • List the exact properties you transition (transition: translate 200ms ease-out) instead of all, so theme changes and layout properties don’t animate by accident.
  • Use transitions for changes between two states and @keyframes for multi-step or looping motion.
  • @starting-style defines the values an element transitions from when it first appears, which makes entry animations from display: none possible in CSS.
  • Wrap non-essential motion in @media (prefers-reduced-motion: no-preference), so people who asked for less motion get a calm interface by default.

Further reading

Check your understanding

3 questions

Question 1 of 3

A card has transition: all 300ms. When the user switches to dark mode, the whole page fades slowly between colors. What’s the cleanest fix?

Question 2 of 3

A toast is shown by switching from display: none to display: block, and its opacity transition never runs. Why?

Question 3 of 3

Which approach best serves users who set ‘reduce motion’ in their OS?

Practice

Code playground

Motion with manners

Add motion to the card, kindly:

  1. Transition the hover lift: animate translate (or transform) and box-shadow only, never all, with durations between 100ms and 400ms.
  2. Create a @keyframes animation that makes the .live-dot::after halo pulse forever, using only cheap properties (scale, opacity, transform, translate, rotate).
  3. Make the transition and the pulse respect prefers-reduced-motion (the opt-in pattern with no-preference is recommended).
<article class="session-card">
  <p class="status"><span class="live-dot" aria-hidden="true"></span> Live now</p>
  <h3>Motion With Manners</h3>
  <p>Transitions, keyframes and reduced motion.</p>
</article>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • The hover transitions translate and box-shadow, not all
  • Transition durations are between 100ms and 400ms
  • The Live dot halo pulses forever
  • The keyframes animate only cheap properties
  • The motion respects prefers-reduced-motion