The transitions guide covers the tool for animating between two states: a base state, a triggered state like hover, and a transition telling the browser to travel between them smoothly instead of snapping. This workbook runs the four longhand properties, durations that feel right, timing functions, delays and staggering, multi-property and per-property transitions, the transform pairing that performs best, and a JavaScript-triggered fade. The one placement rule that governs everything arrives in Example 1: the transition lives on the base state, not the hover.
1. Your first transition
A colour change on hover normally snaps. One transition line on the base state makes it glide, and because it sits on the base state, the return journey animates too.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <button class="button">Hover me</button></body></html>
body { font-family: sans-serif; padding: 3rem; }.button { padding: 0.9rem 1.6rem; border: 0; border-radius: 8px; color: white; font-size: 1rem; background-color: #1f4e78; transition: background-color 0.5s ease-in-out; /* on the BASE state */}.button:hover { background-color: #b71c1c; /* the destination; no transition needed here */}
Hover on and off and notice both directions are smooth. That is the placement rule at work: had the transition sat inside :hover, the colour would glide in but snap back the instant your cursor left. Base state carries the transition; triggered states just declare destinations.
2. The four longhand properties
The shorthand hides four separate controls. Spelling them out once makes every later shorthand readable: which property, how long, with what curve, after what pause.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <div class="panel">Hover me: colour changes after a short pause.</div></body></html>
body { font-family: sans-serif; padding: 3rem; }.panel { padding: 1.5rem; border-radius: 8px; background-color: #e8eef5; transition-property: background-color; /* WHICH property animates */ transition-duration: 0.4s; /* HOW LONG the journey takes */ transition-timing-function: ease-out; /* the acceleration curve */ transition-delay: 0.2s; /* pause BEFORE starting */}.panel:hover { background-color: #1f4e78; color: white;}
Hover and hold: nothing happens for 0.2 seconds, then the colour eases over 0.4 seconds. Note that only background-color is listed in transition-property, so the text colour still snaps instantly, proof that a transition animates exactly what you name and nothing else.
3. Duration: finding the right speed
Duration is feel. The guide’s guidance: 0.2 to 0.4 seconds for snappy interface feedback, 0.5 to 1 second for softer movements. Three identical buttons at different speeds make the difference obvious.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <button class="btn fast">0.15s: almost invisible</button> <button class="btn right">0.3s: feels responsive</button> <button class="btn slow">1.5s: feels sluggish</button></body></html>
body { font-family: sans-serif; padding: 3rem; display: flex; gap: 1rem; }.btn { padding: 0.9rem 1.4rem; border: 0; border-radius: 8px; color: white; background-color: #1f4e78;}.fast { transition: background-color 0.15s; }.right { transition: background-color 0.3s; }.slow { transition: background-color 1.5s; }.btn:hover { background-color: #b71c1c; }
Hover each in turn. The fast one barely registers as animated, the slow one makes the interface feel like it is wading through treacle, and 0.3 seconds sits in the pocket where feedback feels alive but never keeps you waiting. For everyday UI, when in doubt, start at 0.3s.
4. Timing functions side by side
The timing function is the personality of the movement. The same slide with three curves shows why ease-out is the usual choice for interface feedback.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <div class="lane linear">linear</div> <div class="lane ease-in-out">ease-in-out</div> <div class="lane ease-out">ease-out</div> <p>Hover anywhere on the page body to slide all three.</p></body></html>
body { font-family: sans-serif; padding: 3rem; }.lane { width: 140px; padding: 0.75rem; margin-bottom: 0.75rem; background: #d9e2ec; border-radius: 6px; transition-property: transform; transition-duration: 0.8s;}.linear { transition-timing-function: linear; } /* constant, mechanical */.ease-in-out { transition-timing-function: ease-in-out; } /* slow, fast, slow */.ease-out { transition-timing-function: ease-out; } /* fast start, gentle stop */body:hover .lane { transform: translateX(260px); }
Watch the race: linear moves like a machine, ease-in-out breathes in and out, and ease-out lunges then settles, which is exactly how physical objects respond to a push. For custom personalities, cubic-bezier(0.2, 0.8, 0.2, 1) and friends replace the keywords, but the three shown here cover most real interfaces.
5. Delay and staggered reveals
transition-delay holds a transition back, and giving successive elements growing delays turns a flat simultaneous change into a choreographed sequence.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <nav class="menu"> <span class="trigger">Hover the menu</span> <a class="item" href="#">Dashboard</a> <a class="item" href="#">Reports</a> <a class="item" href="#">Settings</a> </nav></body></html>
body { font-family: sans-serif; padding: 3rem; }.menu { max-width: 220px; }.trigger { display: block; padding: 0.75rem 1rem; background: #1f4e78; color: white; border-radius: 6px;}.item { display: block; padding: 0.6rem 1rem; margin-top: 0.4rem; background: #e8eef5; border-radius: 6px; color: #11243a; text-decoration: none; opacity: 0; transform: translateY(-8px); transition: opacity 0.3s ease-out, transform 0.3s ease-out;}.menu:hover .item { opacity: 1; transform: translateY(0); }.menu:hover .item:nth-child(2) { transition-delay: 0.05s; }.menu:hover .item:nth-child(3) { transition-delay: 0.15s; }.menu:hover .item:nth-child(4) { transition-delay: 0.25s; }
Hover the menu and the three items cascade in, each 0.1 seconds behind the last. One subtlety: the delays are declared inside the :hover rules, so the stagger applies on the way in, but leaving hover lets everything retreat together immediately, which feels far better than a delayed exit.
6. Multiple properties, and why not all
Transition several properties by listing them comma-separated. The all keyword works too, but it tells the browser to watch every animatable property, which costs performance and can animate things you never intended.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <button class="explicit">Explicit list (preferred)</button> <button class="everything">all (convenient, riskier)</button></body></html>
body { font-family: sans-serif; padding: 3rem; display: flex; gap: 1rem; }button { padding: 0.9rem 1.4rem; border: 2px solid #1f4e78; border-radius: 8px; background: white; color: #1f4e78;}.explicit { /* name exactly what should animate */ transition: background-color 0.3s ease-out, color 0.3s ease-out;}.everything { transition: all 0.3s ease-out; /* watches EVERY property change */}button:hover { background-color: #1f4e78; color: white; border-radius: 24px; /* only .everything animates this */}
Both buttons animate colour, but only the all button also morphs its corner radius, because all sweeps up every change including ones added to the hover rule months later. The explicit list is a contract: these two properties animate, nothing else ever surprises you. Convenient for prototypes, all; explicit lists for anything you ship.
7. Per-property timing
Each property in the list can carry its own duration, curve, and delay, letting one hover trigger two movements with different characters.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <div class="card"> Quick colour, slow drift: two personalities in one hover. </div></body></html>
body { font-family: sans-serif; padding: 3rem; }.card { max-width: 300px; padding: 1.75rem; border-radius: 10px; background-color: #e8eef5; /* property duration curve delay */ transition: background-color 0.2s ease-out, transform 0.8s ease-in-out 0.1s;}.card:hover { background-color: #1f4e78; color: white; transform: translateX(24px);}
Hover and watch two clocks run at once: the colour flips almost immediately while the card drifts sideways over most of a second, starting slightly late. Splitting the timing per property is how polished interfaces layer a fast acknowledgement over a slower, softer movement without any JavaScript.
8. The transform pairing: a card lift
The best-performing transitions animate transform and opacity, which the browser can hand to the GPU, rather than layout properties like width or margin that force the page to reflow. The classic card lift shows the pattern.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <div class="card">Hover me: I lift, and only cheap properties animate.</div></body></html>
body { font-family: sans-serif; padding: 3rem; background: #f7f9fc; }.card { max-width: 280px; padding: 1.75rem; border-radius: 10px; background: white; box-shadow: 0 1px 3px rgba(17, 35, 58, 0.16); transition: transform 0.25s ease-out, box-shadow 0.25s ease-out;}.card:hover { transform: translateY(-6px) scale(1.01); box-shadow: 0 12px 32px rgba(17, 35, 58, 0.22);}
The card rises, grows a hair, and its shadow deepens, three cues agreeing that it moved toward you. transform handles the movement instead of animating top or margin, so the browser never recalculates the page layout mid-animation, and the result stays smooth even on modest hardware. This pairing is the workhorse of modern interface motion.
9. JavaScript-triggered fades
Transitions are not hover-only: they fire on any property change, including a class toggled by JavaScript. That makes a CSS transition the cheapest possible fade-in, with the script doing nothing but flipping a class.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <button id="toggle">Show the panel</button> <div class="panel" id="panel">Faded in by a class toggle, animated by CSS.</div> <script> const panel = document.getElementById("panel"); document.getElementById("toggle").addEventListener("click", () => { panel.classList.toggle("visible"); /* JS flips the class... */ }); </script></body></html>
body { font-family: sans-serif; padding: 3rem; }button { padding: 0.75rem 1.25rem; border: 0; border-radius: 8px; background: #1f4e78; color: white;}.panel { margin-top: 1rem; padding: 1.5rem; border-radius: 8px; background: #e8eef5; opacity: 0; transform: translateY(8px); transition: opacity 0.4s ease-out, transform 0.4s ease-out; /* ...CSS animates */}.panel.visible { opacity: 1; transform: translateY(0);}
Click the button and the panel fades and rises in; click again and it retreats. The JavaScript is one line of class-toggling with zero animation logic, and the CSS owns the motion, which keeps the two concerns cleanly separated. This is the standard architecture for show-and-hide effects across the whole modern web.
10. Putting it together, respectfully
The finale combines the workbook, a lifted card with staggered inner reveals and per-property timing, and adds the accessibility guard every transition-heavy page needs: prefers-reduced-motion.
<html lang="en"><head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"></head><body> <div class="product"> <h3>Wireless Keyboard</h3> <p class="detail">Low-profile keys, three-device pairing.</p> <span class="detail price">£79.99</span> </div></body></html>
body { font-family: sans-serif; padding: 3rem; background: #f7f9fc; }.product { max-width: 280px; padding: 1.75rem; border-radius: 10px; background: white; box-shadow: 0 1px 3px rgba(17, 35, 58, 0.16); transition: transform 0.25s ease-out, box-shadow 0.25s ease-out;}.product h3 { margin: 0; }.detail { display: block; margin-top: 0.5rem; opacity: 0; transform: translateY(6px); transition: opacity 0.3s ease-out, transform 0.3s ease-out;}.price { font-weight: 700; color: #1f4e78; }.product:hover { transform: translateY(-6px); box-shadow: 0 12px 32px rgba(17, 35, 58, 0.22);}.product:hover .detail { opacity: 1; transform: translateY(0); }.product:hover .price { transition-delay: 0.1s; }/* users who asked for less motion get instant, calm state changes */@media (prefers-reduced-motion: reduce) { .product, .detail { transition: none; } .detail { opacity: 1; transform: none; }}
Hover the card: it lifts on the fast GPU-friendly pair, the description fades in, and the price follows a tenth of a second later. Then enable “reduce motion” in your OS accessibility settings and reload: everything still works, just without the movement. One closing distinction from the guide is worth keeping: transitions travel between two states and need a trigger, while keyframe animations choreograph many states and can loop on their own, so when a hover, focus, or class change defines your start and end, a transition is the right tool, and anything more theatrical belongs to @keyframes.
Work through these and you have the whole article in practice: the base-state placement rule, the four longhand properties, durations that feel right, timing-function personalities, delays and staggering, explicit property lists over all, per-property timing, the transform-and-opacity performance pairing, JavaScript-triggered fades, and the reduced-motion guard. The habit that matters most is the first one: put the transition on the base state, name exactly the properties that should move, and both directions of every interaction come out smooth.
See you soon.
[…] Smooth UI Changes with CSS Transitions: 10 Code-Along Examples […]
[…] Smooth UI Changes with CSS Transitions: 10 Code-Along Examples […]