Topic 10.1
Transitions
In one line
A transition tells the browser to animate a property smoothly from its old value to its new value whenever that value changes, with a chosen duration, timing function and delay; only properties whose values can be blended in between can transition.
Think of it like this
A dimmer switch. A normal light switch jumps from dark to bright in an instant. A dimmer turns the light up gradually over a second or two. Without a transition, CSS is the normal switch: hover a button and its colour jumps. A transition is the dimmer: the browser fills in all the colours in between, so the change glides.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Transition
- An instruction to animate a property smoothly from its old value to its new value when the value changes.
- Duration
- How long the transition takes, like
0.3s(seconds) or300ms(milliseconds). - Delay
- How long to wait before the transition starts moving.
- Timing function (easing)
- A curve that says how the speed changes during the transition: slow start, slow end, constant, or in steps.
- Cubic Bézier
- A smooth curve set by two control points. Every easing keyword like
easeis one of these. - Interpolate
- Work out the values in between a start and an end, like 150px halfway from 100px to 200px.
- Animatable property
- A property the browser is able to animate, such as
opacity,widthorcolor. - Discrete value
- A value with nothing in between, like
display: noneorblock. It can only jump. - Shorthand and longhand
- A shorthand like
transitionsets several properties at once; each longhand liketransition-delaysets one. - `transitionend`
- The event the browser fires on an element when one of its transitions finishes.
Step by step
01Start with an instant change
This button turns orange on hover. With no transition, the colour jumps in a single frame. It works, but it feels abrupt.
.btn {
background-color: navy;
color: white;
}
.btn:hover {
background-color: darkorange;
}02Add a transition to the normal state
Put the transition on .btn, not on .btn:hover. Then the colour glides both ways: in when you hover and back out when you leave. If you put it only on :hover, the change in is smooth but the change back is instant, because when the mouse leaves, the :hover rule (and its transition) no longer applies.
.btn {
background-color: navy;
color: white;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: darkorange;
}03Pick the easing and the delay
The shorthand order is property, duration, timing function, delay. ease-out feels natural for things that react to the user (fast response, gentle stop). A short delay can stop a menu opening when the mouse only passes over it.
.menu {
opacity: 0;
transition: opacity 0.2s ease-out 0.15s;
/* property duration easing delay */
}
.nav:hover .menu {
opacity: 1;
}04Transition several properties
List each one, separated by commas, each with its own timing. Avoid transition: all in real code: it also animates properties you didn't mean to (a colour change from a theme switch, a size change on resize) and makes the browser check every property on every change.
.card {
transition:
transform 0.25s ease-out,
box-shadow 0.25s ease-out,
background-color 0.4s linear;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgb(0 0 0 / 0.2);
}05Know what happens in time
When the value changes, the browser creates the transition (transitionrun), waits for the delay, starts moving (transitionstart), and on every frame computes an in-between value using the easing curve. At the end it fires transitionend and the property simply has its new value.
06Make an element fade in when it appears
A brand-new element has no "before" style, so a plain transition can't run. @starting-style gives the browser the value to start from on the element's first style. Combined with transition-behavior: allow-discrete, it also lets you fade elements in and out of display: none.
.toast {
opacity: 1;
transition: opacity 0.3s, display 0.3s allow-discrete;
}
@starting-style {
.toast { opacity: 0; } /* where the fade-in starts */
}
.toast.hidden {
display: none;
opacity: 0; /* fades out, then display flips at the end */
}Try it yourself
- 1
Move the transition to :hover
In the first example, cut
transition: ...from.smoothand paste it into a new rule.smooth:hover { ... }. Predict what happens when the mouse leaves the button, then check. (The way back is instant, because the transition only exists while hovering.) - 2
Change the delay and predict the events
In the second example, change the delay to
0s. Predict whethertransitionstartstill fires and what eachelapsedTimeprints. Then try a negative delay,-0.1s: the transition starts as if it had already been running for 0.1s, sotransitionstartreports0.1. - 3
Rescue the height transition
In the third example, change
.on .b { height: auto; }toheight: 40pxand predict the new count forb. A fixed length can be interpolated, so the count becomes1. - 4
Invent an easing
In the fourth example, add a seventh track with
--e: linear(0, 0.6 30%, 1 60%, 0.9 80%, 1). Predict its motion from the numbers (fast to 60%, reaches the end at 60% of the time, dips back, settles), then hover.
Code & diagrams
Move the mouse over each button and away again. The transition is written on `.btn`'s normal state, so it plays both ways.
Preview
Console
Nothing logged yet.`transitionrun` fires as soon as the class changes, `transitionstart` after the 0.1s delay, `transitionend` when the box reaches 200px. `elapsedTime` never counts the delay, so it is 0 at the start and the duration (0.3) at the end. Delete the `box.offsetWidth` line: the box may skip straight to 200px, because the browser never saw the 100px value.
Preview
Console
Nothing logged yet.Expected console output
property: width | duration: 0.3s
easing: ease-out | delay: 0.1s
transitionrun width 0
transitionstart width 0
transitionend width 0.3
final width: 200pxOnly real in-between values (a) transition by default. `auto` (b) and `display` (c) jump. With `allow-discrete` (d), a `display` transition runs: the element stays visible for 0.2s and then disappears, which is what lets an exit fade finish.
Preview
Console
Nothing logged yet.Expected console output
a (length to length): 1
b (length to auto): 0
c (display, default): 0
d (display, allow-discrete): 1All six move the same distance in the same 1.2s; only the speed along the way differs. The custom property `--e` passes each easing into one shared rule.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Transition to height: auto
Write .panel { height: 0; overflow: hidden; transition: height 0.3s; } and .panel.open { height: auto; }.
Break #2
Add the class in the same moment the element is created
Create an element with document.createElement, append it, and immediately add a class that changes its opacity from 0 to 1.
Break #3
Wait for transitionend that never comes
Write code that removes an element only inside a transitionend listener, then set the transition duration to 0s (or let the user switch on reduced motion, which your CSS turns into 0s).
Myth vs fact
Myth
Put the transition on the :hover rule.
Fact
Put it on the element's normal state. A transition on :hover only applies while hovering, so the change back happens instantly.
Myth
transition: all is a handy default.
Fact
It animates every property that changes, including ones you never meant to animate, and makes the browser consider every property on every style change. List the properties you want.
Myth
Anything can be transitioned if you set a duration.
Fact
Only animatable properties with interpolable values transition. display, visibility-like keywords, auto sizes and font-family jump, unless newer features like allow-discrete or interpolate-size handle them.
Myth
transitionend always fires once per transition.
Fact
It fires once per property that transitioned (a border change can fire it four times, once per side's longhand), and not at all if the transition is cancelled or has zero duration.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
When a transitioned value is reversed partway, the spec applies a reversing shortening factor: the reverse transition's duration is scaled by how far the first one got, so un-hovering after a quarter of the way takes about a quarter of the time. It's why quick hover-on/hover-off feels right without any code.
- ▸
Transitions are created during style change events, which happen when the browser recalculates style (before a frame, or when script forces it by reading
offsetWidth,getComputedStyle(...).widthand similar). Two writes with no style calculation in between look like one change to the browser. - ▸
CSS transitions and animations are exposed through the Web Animations API:
el.getAnimations()returnsCSSTransitionandCSSAnimationobjects you can pause, reverse, seek (currentTime) or await (.finished). That's more reliable than countingtransitionendevents, especially when several longhands transition. - ▸
Transitioning
transformandopacitycan run on the compositor thread and stay smooth even when the main thread is busy; transitioningwidth,height,topormargincauses layout work every frame (Topic 10.5). For colours, the interpolation happens in the colour space given by the spec (sRGB for legacy colour syntax, Oklab by default forcolor-mix()and modern gradients' interpolation hints).
Remember this
- 1
A transition is an instruction that says: "when this property's value changes, don't jump; move from the old value to the new value over some time." You write it on the element before anything changes, usually in its normal rule:
.btn { background: navy; transition: background-color 0.3s; }. Then anything that changes the value (a:hoverrule, a class added by JavaScript, a media query) is animated. The transition is not the change itself; it only says how changes happen. - 2
A transition has four parts, each with its own longhand property (a property that sets one thing):
transition-property(which property to watch, such asopacity, orall),transition-duration(how long, in seconds0.3sor milliseconds300ms; the default is0s, which means no transition),transition-timing-function(how the speed changes along the way) andtransition-delay(how long to wait before starting). Thetransitionshorthand sets all four:transition: opacity 0.3s ease-in 0.1s. The first time value is always the duration and the second is the delay. Several transitions are separated by commas:transition: opacity 0.2s, transform 0.4s ease-out. - 3
The timing function (also called easing) controls the speed during the transition.
ease(the default) starts a little slowly, speeds up and slows down at the end.linearmoves at one constant speed.ease-instarts slow and finishes fast;ease-outstarts fast and slows to a stop;ease-in-outis slow at both ends. Each of these keywords is really a cubic Bézier curve, a smooth curve defined by two control points, and you can write your own:easeiscubic-bezier(0.25, 0.1, 0.25, 1). Values above 1 give an overshoot (a small bounce past the target).steps(4)jumps in 4 equal steps with no smoothness, andlinear(0, 0.8, 1)(Baseline 2023) draws an easing from a list of points, which can describe springs and bounces. - 4
What can be transitioned? Only properties that are animatable, and only between values the browser can interpolate (blend in between). Lengths, colours, numbers, percentages,
opacity,transformand shadows all interpolate: halfway from100pxto200pxis150px. Some values have no "in between":display: nonetoblock,visibility,font-family, or a length to the keywordauto(height: 0toheight: auto). Those discrete changes jump at once, and by default no transition runs at all. Newer CSS can help:transition-behavior: allow-discrete(Baseline 2024) letsdisplayand similar take part, flipping at the right moment, and@starting-style(Baseline 2024) gives a just-appeared element a starting value to transition from. - 5
A transition only runs when the computed value changes after the element already has a style. On the very first render there is no "before" value, so nothing animates; that's why adding a class in the same moment the element is created often shows no transition. Browsers batch style changes, so if you change a value and change it back before the next style calculation, nothing happens. To start from a known value, read a layout value such as
el.offsetWidthfirst (this forces a style recalculation), then change the class; or use@starting-style. - 6
Transitions fire events you can listen to from JavaScript:
transitionrun(the transition was created, even if it's still waiting for its delay),transitionstart(the delay is over and it's moving),transitionend(it reached the end) andtransitioncancel(it was stopped, for example because the property changed again or the element gotdisplay: none). Each event haspropertyNameandelapsedTime(seconds). If you change the value back halfway through, the browser starts a reverse transition from the current value, and shortens it so it doesn't take the full duration to travel half the distance.
Explain it without notes
What is a CSS transition, and what are its four parts?
Why should a hover transition be declared on the element's normal state rather than on :hover?
Which properties can be transitioned, and why can't height: auto or display transition by default?
Why does a transition sometimes not run when you add a class from JavaScript, and how do you fix it?
What do transitionrun, transitionstart, transitionend and transitioncancel mean?
Practice
Make a link whose underline colour fades from transparent to crimson over 0.25s when hovered, and fades back when the mouse leaves.
Write a .tooltip that is invisible (opacity: 0) and fades in over 0.2s, but only after a 0.4s delay, when its parent .has-tip is hovered. It should disappear instantly (no delay) when the mouse leaves.
Log done to the console when a .bar finishes transitioning its width from 0 to 100%, triggered by adding a class full in JavaScript.
Trade-offs
- ↔
Transitions are simple and declarative, but they only animate between two states and only when something changes; for multi-step or looping motion, use keyframe animations (Topic 10.3).
- ↔
Longer durations feel smooth in a demo but slow in daily use: most UI feedback should be 100–300ms. Users who repeat an action many times notice every extra 100ms.
- ↔
Workarounds for
height: auto(max-height, grid rows, JavaScript measuring) each trade accuracy, simplicity or support;interpolate-sizeis the clean answer once all browsers have it.
Done when you can
I can write a transition with property, duration, easing and delay, and read the shorthand.
I put transitions on the normal state so they play both ways.
I can say which properties interpolate and why
autoanddisplaydon't by default.I can make a transition run from JavaScript by forcing a style calculation or using
@starting-style.I can listen for
transitionendand know when it won't fire.I can pick an easing (
ease-out,cubic-bezier,steps,linear()) for a purpose.