Lesson 6
CSS Transitions
A transition is the simplest form of CSS animation: you define a start state and an end state, and the browser interpolates between them over time. No keyframes, no timeline - just "when this property changes, do it smoothly."
The four parts of a transition
transition: <property> <duration> <timing-function> <delay>;
/* Example */
transition: background 0.3s ease 0s;
/* Shorthand with multiple properties */
transition:
background 0.3s ease,
transform 0.2s ease-out,
box-shadow 0.4s ease;
transition-property- which property to animate (all, a specific name, or a comma-separated list)transition-duration- how long the interpolation takes (e.g.0.3s,300ms)transition-timing-function- the acceleration curvetransition-delay- how long to wait before starting (default0s)
Where to put it: The transition declaration goes on the base state, not the hover/active state. This way it applies in both directions - entering AND leaving the state change.
Why? The transition declaration is only in effect while its selector matches. If you put it on :hover, then when the mouse leaves, the :hover block deactivates - taking the transition instruction with it. The property snaps back instantly because there's no transition in effect for the "return trip."
Asymmetric transitions - different timing for enter vs leave
You can exploit this behaviour intentionally. By putting different transition values on the base and hover states, you get different speeds for each direction:
.button {
background: blue;
transition: background 0.6s ease; /* governs the LEAVE (slow fade back) */
}
.button:hover {
background: coral;
transition: background 0.1s ease; /* governs the ENTER (fast snap on) */
}
When you hover, the :hover transition kicks in (fast 0.1s). When you leave, the :hover rules disappear, and the base state's transition takes over (slow 0.6s). This gives you a fast snap-in, slow fade-out effect - useful for things like button highlights that should feel responsive on hover but gentle on leave.
Live demo: basic transition
Hover to transition width and background simultaneously
<!-- HTML -->
<div class="basic-box">Hover me</div>
/* CSS */
.basic-box {
width: 100px;
height: 60px;
background: var(--accent);
border-radius: 6px;
transition: width 0.4s ease, background 0.4s ease;
}
.basic-box:hover {
width: 300px;
background: #e76f51;
}
Timing functions - the acceleration curve
The timing function controls how the value changes over time. It doesn't change the start/end - just how you get there.
Hover anywhere in this box - all bars transition to full width with different timing
<!-- HTML -->
<div class="timing-row">
<div class="timing-bar">ease</div>
<div class="timing-bar">linear</div>
<div class="timing-bar">ease-in</div>
<div class="timing-bar">ease-out</div>
<div class="timing-bar">ease-in-out</div>
<div class="timing-bar">cubic-bezier</div>
</div>
/* CSS */
.timing-bar:nth-child(1) { transition: width 1s ease; }
.timing-bar:nth-child(2) { transition: width 1s linear; }
.timing-bar:nth-child(3) { transition: width 1s ease-in; }
.timing-bar:nth-child(4) { transition: width 1s ease-out; }
.timing-bar:nth-child(5) { transition: width 1s ease-in-out; }
.timing-bar:nth-child(6) { transition: width 1s cubic-bezier(0.68, -0.55, 0.27, 1.55); }
.timing-row:hover .timing-bar { width: 100%; }
The built-in keywords:
ease- starts fast, slows down at the end (default). Good for most UI transitions.linear- constant speed. Feels mechanical. Use for progress bars or continuous motion.ease-in- starts slow, accelerates. Feels like something being thrown. Good for exit animations.ease-out- starts fast, decelerates. Feels like something arriving. Good for entrance animations.ease-in-out- slow start and end, fast middle. Symmetric. Good for things that move and stop.
cubic-bezier() - custom curves
All timing keywords are shortcuts for cubic-bezier() curves. You can write your own:
/* Bouncy overshoot */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
/* The keywords as cubic-bezier: */
ease: cubic-bezier(0.25, 0.1, 0.25, 1.0)
linear: cubic-bezier(0.0, 0.0, 1.0, 1.0)
ease-in: cubic-bezier(0.42, 0, 1.0, 1.0)
ease-out: cubic-bezier(0, 0, 0.58, 1.0)
ease-in-out: cubic-bezier(0.42, 0, 0.58, 1.0)
Practical tip: Use cubic-bezier.com to visually design curves. Values outside 0 - 1 on the Y axis create overshoot/bounce effects (the bar in the demo above overshoots then snaps back).
Transition delay - staggered effects
A delay makes the transition wait before starting. Combined with different delays per element, you get stagger effects:
Hover the row - each box starts its transition slightly later
<!-- HTML -->
<div class="delay-row">
<div class="delay-box"></div>
<div class="delay-box"></div>
<div class="delay-box"></div>
<div class="delay-box"></div>
<div class="delay-box"></div>
</div>
/* CSS */
.delay-box {
transition: transform 0.3s ease, background 0.3s ease;
}
.delay-box:nth-child(1) { transition-delay: 0s; }
.delay-box:nth-child(2) { transition-delay: 0.1s; }
.delay-box:nth-child(3) { transition-delay: 0.2s; }
.delay-box:nth-child(4) { transition-delay: 0.3s; }
.delay-box:nth-child(5) { transition-delay: 0.4s; }
.delay-row:hover .delay-box {
transform: translateY(-10px);
background: teal;
}
Multiple properties - independent timing
You can give each property its own duration, timing, and delay:
Hover - each property transitions at its own speed
<!-- HTML -->
<div class="multi-box">Hover</div>
/* CSS */
.multi-box {
width: 100px;
height: 60px;
background: var(--accent);
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition:
background 0.3s ease,
transform 0.2s ease,
box-shadow 0.3s ease,
border-radius 0.4s ease;
}
.multi-box:hover {
background: #e76f51;
transform: scale(1.1);
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
border-radius: 50%;
}
transition: all - convenient but risky
/* Transitions every animatable property */
transition: all 0.3s ease;
This is tempting but has downsides:
- Transitions properties you didn't intend (e.g.
heightchanging when content updates) - Can cause performance issues if expensive properties animate unexpectedly
- Makes it harder to give different properties different timings
Best practice: Be explicit. List the properties you want to transition. Use all only in prototyping or when you genuinely want everything to animate.
What can and can't be transitioned
A property can be transitioned if the browser knows how to interpolate between its values. Most numeric/colour properties work:
- ✓
opacity,transform,color,background-color,width,height,padding,margin,border-radius,box-shadow,font-size - ✓ Registered custom properties (via
@property- you learned this in Lesson 2) - ✗
display- historically a discrete swap (can't interpolate betweenblockandnone) - ✗
font-family- no interpolation between typefaces - ✗
background-image- can't interpolate between different images (but CAN crossfade gradients in some cases)
New: transition-behavior: allow-discrete
A recent addition (Chrome 117+) lets you transition properties that would normally swap discretely. The most useful case: transitioning display so elements can fade out before being removed:
.box {
transition: opacity 0.4s ease, display 0.4s ease allow-discrete;
}
.box.hidden {
opacity: 0;
display: none; /* transitions at the END of the opacity fade */
}
Without allow-discrete, adding display: none would instantly hide the element before the opacity had a chance to fade. With it, the display change waits until the transition finishes.
Transitioning custom properties (callback to Lesson 2)
Remember: unregistered custom properties can't be transitioned. But with @property, you can transition anything that has a numeric component inside it. Here's a gradient angle rotating on hover:
Hover - the gradient angle transitions from 0deg to 180deg
@property --gradient-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
<!-- HTML -->
<div class="gradient-angle-box">Hover me</div>
/* CSS */
.gradient-angle-box {
background: linear-gradient(var(--gradient-angle), blue, coral);
transition: --gradient-angle 0.8s ease-in-out;
}
.gradient-angle-box:hover {
--gradient-angle: 180deg;
}
The pattern: Extract → Register → Transition. Pull a numeric part out of a compound value into a custom property, register it with @property, then transition the property. Works for gradient stops, hues, angles, individual transform values - anything.
Performance: what's cheap to transition
Not all transitions are created equal. The browser renders in layers, and some property changes are much cheaper than others:
| Cost | Properties | Why |
|---|---|---|
| Cheap (composite) | transform, opacity |
GPU-accelerated. No layout or paint needed. |
| Medium (paint) | color, background, box-shadow, border |
Triggers repaint but no layout recalculation. |
| Expensive (layout) | width, height, padding, margin, top/left |
Triggers layout recalculation for the element AND its neighbours. |
Rule of thumb: If you can achieve the same visual effect with transform (e.g. scale() instead of changing width, translateY() instead of changing top), prefer transform. It's composited on the GPU and won't trigger layout thrash.
Common gotchas
1. Transitioning height to/from auto
You can't transition height: 0 → height: auto because the browser can't interpolate to an unknown value. Workarounds:
- Use
max-heightwith an estimated value (hacky, can feel off) - Use
transform: scaleY(0)→scaleY(1)(visual only, doesn't affect layout) - Use the new
calc-size()function:height: calc-size(auto)(Chrome 129+, allows transitioning to auto) - Use
grid-template-rows: 0fr→1fr(works today, transitions the grid track)
2. Transition on page load
Transitions don't fire on initial page load - they only trigger when a property changes. If you want an entrance animation, use @keyframes (Lesson 7).
3. Transitioning to display: none
Covered above - use transition-behavior: allow-discrete or the classic workaround: transition opacity to 0 first, then toggle display in a transitionend event handler.
Retrieval check
Question 1
Where should the transition declaration go - on the base state or the hover/active state?
Question 2
Which two properties are cheapest to transition (GPU composited)?
Question 3
You want an element to start moving slowly and accelerate. Which timing function?
Question 4
Can you transition height from 0 to auto?
Question 5
What does transition-behavior: allow-discrete enable?