{"id":"l-css-4-1","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"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.","html":"<p>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.</p>\n<p>This lesson covers the tools (transitions, keyframes, <code dir=\"ltr\">@starting-style</code>) and the responsibility (<code dir=\"ltr\">prefers-reduced-motion</code>).</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Transitions: between two states</h2>\n<p>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:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">session-card.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    box-shadow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:hover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-within</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    box-shadow</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 16</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rgb</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">15</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 23</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> / </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Three choices are doing work here. <strong>Specific properties</strong>: the card lifts and its shadow grows, nothing else animates. <strong>Short duration</strong>: 150–250ms feels responsive for small UI changes; anything over 400ms feels sluggish when the user is waiting on it. <strong>Easing</strong>: <code dir=\"ltr\">ease-out</code> starts fast and settles, which suits things responding to the user.</p>\n<p>The individual transform properties (<code dir=\"ltr\">translate</code>, <code dir=\"ltr\">scale</code>, <code dir=\"ltr\">rotate</code>) are worth using over <code dir=\"ltr\">transform</code>: they can be transitioned and overridden independently, so a hover lift doesn’t wipe out a scale set somewhere else.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>transition: all</span></p>\n<p><code dir=\"ltr\">transition: all 300ms</code> animates every property that changes, including ones you never thought about. Switch to dark mode and every color on the card fades slowly; change padding at a breakpoint and the card visibly stretches. List the properties you mean. It’s more typing and far less surprise.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Keyframes: multi-step and looping motion</h2>\n<p>When motion has more than two steps, or repeats on its own, you need <code dir=\"ltr\">@keyframes</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@keyframes</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> pulse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  0%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">   { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;   </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  70%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  100%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">scale</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2.2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.live-dot::after</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  content</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">absolute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border-radius</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">50</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">currentColor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: pulse </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> infinite</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The dot’s halo grows and fades, then pauses briefly (the 70%–100% hold) before repeating. Animating <code dir=\"ltr\">scale</code> and <code dir=\"ltr\">opacity</code> is deliberate: those two can run on the compositor without re-running layout, which the last lesson in this course explains.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Entry animations with @starting-style</h2>\n<p>The save toast is <code dir=\"ltr\">display: none</code> 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. <code dir=\"ltr\">@starting-style</code> provides one:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    opacity </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    display </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> allow-discrete;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@starting-style</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    translate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.toast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>When <code dir=\"ltr\">hidden</code> is removed, the browser starts the toast at the <code dir=\"ltr\">@starting-style</code> values and transitions to the normal ones. <code dir=\"ltr\">transition-behavior: allow-discrete</code> (here written as <code dir=\"ltr\">allow-discrete</code> in the shorthand) lets <code dir=\"ltr\">display</code> flip at the right moment on the way out, so the fade-out is visible before the element disappears. <code dir=\"ltr\">@starting-style</code> and <code dir=\"ltr\">transition-behavior</code> are Baseline 2024, so the entry animation works everywhere current. Animating <code dir=\"ltr\">display</code> 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.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Respect reduced motion</h2>\n<p>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 <code dir=\"ltr\">prefers-reduced-motion</code>.</p>\n<p>The most robust pattern makes motion opt-in:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prefers-reduced-motion: no-preference) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    transition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: translate </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, box-shadow </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ms</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .live-dot::after</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    animation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: pulse </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.6</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">s</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ease-out</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> infinite</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>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.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why reduce, not remove</span></p>\n<p>Reduced motion doesn’t mean no feedback. A state change that slides 200px across the screen can become a quick fade; a hover lift can become a border-color change. Keep the meaning, drop the movement. And never make information depend only on animation: the Live dot also has the text “Live now” next to it.</p>\n</aside>\n<p>You’ll also see a global safety net that sets every animation and transition duration to almost zero inside <code dir=\"ltr\">@media (prefers-reduced-motion: reduce)</code>. 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 <code dir=\"ltr\">0s</code> transition never fires <code dir=\"ltr\">transitionend</code>), and it removes harmless fades along with harmful motion.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Your turn</h2>\n<p>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.</p>\n","toc":[{"id":"sec-1","text":"Transitions: between two states","level":2},{"id":"sec-2","text":"Keyframes: multi-step and looping motion","level":2},{"id":"sec-3","text":"Entry animations with @starting-style","level":2},{"id":"sec-4","text":"Respect reduced motion","level":2},{"id":"sec-5","text":"Your turn","level":2}],"takeaways":["List the exact properties you transition (<code dir=\"ltr\">transition: translate 200ms ease-out</code>) instead of <code dir=\"ltr\">all</code>, so theme changes and layout properties don’t animate by accident.","Use transitions for changes between two states and <code dir=\"ltr\">@keyframes</code> for multi-step or looping motion.","<code dir=\"ltr\">@starting-style</code> defines the values an element transitions from when it first appears, which makes entry animations from <code dir=\"ltr\">display: none</code> possible in CSS.","Wrap non-essential motion in <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code>, so people who asked for less motion get a calm interface by default."],"further":[{"title":"Using CSS transitions (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Transitions/Using"},{"title":"@starting-style (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@starting-style"},{"title":"prefers-reduced-motion (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion"}],"quiz":[{"q":"<p>A card has <code dir=\"ltr\">transition: all 300ms</code>. When the user switches to dark mode, the whole page fades slowly between colors. What’s the cleanest fix?</p>","options":[{"text":"Remove the transition in dark mode with a media query.","why":"That treats the symptom for one case; any other property change (a padding tweak at a breakpoint) still animates."},{"text":"Set <code dir=\"ltr\">transition-duration: 0s</code> on <code dir=\"ltr\">:root</code>.","why":"Transitions aren’t inherited, so that doesn’t reach the card, and it would disable the hover effect too."},{"text":"Transition only the properties you mean, such as <code dir=\"ltr\">transition: translate 200ms, box-shadow 200ms</code>.","why":"Correct. Listing properties makes the motion intentional; colors and layout change instantly unless you ask for them."}],"answer":2},{"q":"<p>A toast is shown by switching from <code dir=\"ltr\">display: none</code> to <code dir=\"ltr\">display: block</code>, and its <code dir=\"ltr\">opacity</code> transition never runs. Why?</p>","options":[{"text":"Opacity can’t be transitioned on elements that use display.","why":"Opacity transitions fine; the problem is the element has no previous style to transition from."},{"text":"When an element first renders there is no before-state, so there’s nothing to transition from; <code dir=\"ltr\">@starting-style</code> provides one.","why":"Correct. <code dir=\"ltr\">@starting-style { .toast { opacity: 0; } }</code> gives the browser the starting values for the first style update."},{"text":"Transitions only run on hover.","why":"Transitions run on any computed style change, whatever caused it."},{"text":"The duration must be at least 500ms for the browser to notice.","why":"There’s no minimum duration; even 100ms transitions run."}],"answer":1},{"q":"<p>Which approach best serves users who set ‘reduce motion’ in their OS?</p>","options":[{"text":"Put decorative motion inside <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code>, and keep simple fades or instant changes as the default.","why":"Correct. Motion becomes opt-in, and the interface still communicates state changes for everyone."},{"text":"Hide every animated element when reduced motion is on.","why":"Hiding content removes information. Users asked for less motion, not less interface."},{"text":"Slow every animation down to 2 seconds.","why":"Slower movement is still movement, and long animations make the interface feel broken."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<article class=\"session-card\">\n  <p class=\"status\"><span class=\"live-dot\" aria-hidden=\"true\"></span> Live now</p>\n  <h3>Motion With Manners</h3>\n  <p>Transitions, keyframes and reduced motion.</p>\n</article>","css":".session-card {\n  padding: 12px 16px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n  background: white;\n}\n.session-card:hover {\n  translate: 0 -2px;\n  box-shadow: 0 6px 16px rgb(15 23 42 / 0.12);\n}\n.status { margin: 0; color: #b91c1c; font-weight: 600; }\n.live-dot {\n  position: relative;\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  background: currentColor;\n}\n.live-dot::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: 50%;\n  background: currentColor;\n}\n\n/* Add the transition, the pulse animation and reduced-motion handling */\n"},"solution":{"html":"<article class=\"session-card\">\n  <p class=\"status\"><span class=\"live-dot\" aria-hidden=\"true\"></span> Live now</p>\n  <h3>Motion With Manners</h3>\n  <p>Transitions, keyframes and reduced motion.</p>\n</article>","css":".session-card {\n  padding: 12px 16px;\n  border: 1px solid #e5e7eb;\n  border-radius: 12px;\n  background: white;\n}\n.session-card:hover {\n  translate: 0 -2px;\n  box-shadow: 0 6px 16px rgb(15 23 42 / 0.12);\n}\n.status { margin: 0; color: #b91c1c; font-weight: 600; }\n.live-dot {\n  position: relative;\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  background: currentColor;\n}\n.live-dot::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: 50%;\n  background: currentColor;\n}\n\n@keyframes pulse {\n  0% { scale: 1; opacity: 1; }\n  70% { scale: 2.2; opacity: 0; }\n  100% { scale: 2.2; opacity: 0; }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .session-card {\n    transition: translate 200ms ease-out, box-shadow 200ms ease-out;\n  }\n  .live-dot::after {\n    animation: pulse 1.6s ease-out infinite;\n  }\n}\n"},"checks":[{"id":"transition-specific","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return all.some(({ r }) => r.style && /translate|transform/.test(r.style.transitionProperty || r.style.transition || '')); const props = style('.session-card', 'transition-property').split(',').map((s) => s.trim()); return props.some((p) => p === 'translate' || p === 'transform') && props.includes('box-shadow') && !props.includes('all');"},{"id":"transition-duration","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return true; const ds = style('.session-card', 'transition-duration').split(',').map((s) => parseFloat(s) * (s.trim().endsWith('ms') ? 0.001 : 1)); return ds.length > 0 && ds.every((d) => d >= 0.1 && d <= 0.4);"},{"id":"pulse-loops","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); const hasKeyframes = all.some(({ r }) => r instanceof CSSKeyframesRule); if (matchMedia('(prefers-reduced-motion: reduce)').matches) return hasKeyframes; const s = getComputedStyle($('.live-dot'), '::after'); return hasKeyframes && s.animationName !== 'none' && s.animationIterationCount === 'infinite';"},{"id":"pulse-cheap","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); const kf = all.map(({ r }) => r).filter((r) => r instanceof CSSKeyframesRule); if (!kf.length) return false; const allowed = ['scale', 'opacity', 'transform', 'translate', 'rotate']; return kf.every((k) => [...k.cssRules].every((f) => [...f.style].every((p) => allowed.includes(p))));"},{"id":"reduced-motion","test":"const all = []; const walk = (rules, media) => { for (const r of rules) { all.push({ r, media }); if (r.cssRules) walk(r.cssRules, media || (r instanceof CSSMediaRule && r.conditionText.includes('prefers-reduced-motion') ? r.conditionText : '')); } }; for (const s of document.styleSheets) walk(s.cssRules, ''); return all.some(({ r, media }) => media && r.style && (r.style.animation || r.style.animationName || r.style.transition || r.style.transitionProperty || r.style.animationDuration || r.style.transitionDuration));"}],"title":"Motion with manners","prompt":"<p>Add motion to the card, kindly:</p>\n<ol>\n<li>Transition the hover lift: animate <strong><code dir=\"ltr\">translate</code></strong> (or <code dir=\"ltr\">transform</code>) and <strong><code dir=\"ltr\">box-shadow</code></strong> only, never <code dir=\"ltr\">all</code>, with durations between 100ms and 400ms.</li>\n<li>Create a <code dir=\"ltr\">@keyframes</code> animation that makes the <code dir=\"ltr\">.live-dot::after</code> halo pulse <strong>forever</strong>, using only cheap properties (<code dir=\"ltr\">scale</code>, <code dir=\"ltr\">opacity</code>, <code dir=\"ltr\">transform</code>, <code dir=\"ltr\">translate</code>, <code dir=\"ltr\">rotate</code>).</li>\n<li>Make the transition and the pulse respect <strong><code dir=\"ltr\">prefers-reduced-motion</code></strong> (the opt-in pattern with <code dir=\"ltr\">no-preference</code> is recommended).</li>\n</ol>\n","hints":["<code dir=\"ltr\">transition</code> accepts a comma-separated list: <code dir=\"ltr\">translate 200ms ease-out, box-shadow 200ms ease-out</code>.","For the pulse, animate <code dir=\"ltr\">scale</code> and <code dir=\"ltr\">opacity</code> in <code dir=\"ltr\">@keyframes</code>, and use <code dir=\"ltr\">infinite</code> as the iteration count.","Put both the <code dir=\"ltr\">transition</code> and the <code dir=\"ltr\">animation</code> declarations inside <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference) { … }</code>."],"explanation":"<p>Listing <code dir=\"ltr\">translate</code> and <code dir=\"ltr\">box-shadow</code> means only the lift animates; theme or layout changes stay instant. The pulse keyframes touch only <code dir=\"ltr\">scale</code> and <code dir=\"ltr\">opacity</code>, which browsers can animate on the compositor without recalculating layout. Wrapping both inside <code dir=\"ltr\">@media (prefers-reduced-motion: no-preference)</code> makes the motion opt-in: people who asked their OS to reduce motion get a static card and a static dot, while the “Live now” text still tells them what the dot meant.</p>\n","checkLabels":{"transition-specific":"The hover transitions translate and box-shadow, not all","transition-duration":"Transition durations are between 100ms and 400ms","pulse-loops":"The Live dot halo pulses forever","pulse-cheap":"The keyframes animate only cheap properties","reduced-motion":"The motion respects prefers-reduced-motion"}},"words":700,"readMinutes":3}