Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~31 min· topic 1 of 6

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) or 300ms (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 ease is 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, width or color.
Discrete value
A value with nothing in between, like display: none or block. It can only jump.
Shorthand and longhand
A shorthand like transition sets several properties at once; each longhand like transition-delay sets 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.

style.csswhole filecss
.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.

style.csswhole filecss
.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.

style.csswhole filecss
.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.

style.csswhole filecss
.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.

Know what happens in timediagram
Rendering diagram…

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.

style.csswhole filecss
.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. 1

    Move the transition to :hover

    In the first example, cut transition: ... from .smooth and 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. 2

    Change the delay and predict the events

    In the second example, change the delay to 0s. Predict whether transitionstart still fires and what each elapsedTime prints. Then try a negative delay, -0.1s: the transition starts as if it had already been running for 0.1s, so transitionstart reports 0.1.

  3. 3

    Rescue the height transition

    In the third example, change .on .b { height: auto; } to height: 40px and predict the new count for b. A fixed length can be interpolated, so the count becomes 1.

  4. 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

Instant versus smooth New tab

Move the mouse over each button and away again. The transition is written on `.btn`'s normal state, so it plays both ways.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Transition events, in order New tab

`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

Press Run to see the page.

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: 200px
What can and can't transition Baseline 2024+ New tab

Only 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

Press Run to see the page.

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): 1
Easing curves side by side New tab

All 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

Press Run to see the page.

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; }.

terminal
$ the page shows
── what you'll see ──
The panel snaps open and shut instantly. No transitionrun event fires.

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.

terminal
$ the page shows
── what you'll see ──
The element appears at full opacity with no fade. No transition events fire.

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).

terminal
$ the page shows
── what you'll see ──
The element is never removed: it stays on the page, invisible, and can still catch clicks.

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(...).width and 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() returns CSSTransition and CSSAnimation objects you can pause, reverse, seek (currentTime) or await (.finished). That's more reliable than counting transitionend events, especially when several longhands transition.

  • ▸

    Transitioning transform and opacity can run on the compositor thread and stay smooth even when the main thread is busy; transitioning width, height, top or margin causes 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 for color-mix() and modern gradients' interpolation hints).

Remember this

  1. 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 :hover rule, a class added by JavaScript, a media query) is animated. The transition is not the change itself; it only says how changes happen.

  2. 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 as opacity, or all), transition-duration (how long, in seconds 0.3s or milliseconds 300ms; the default is 0s, which means no transition), transition-timing-function (how the speed changes along the way) and transition-delay (how long to wait before starting). The transition shorthand 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. 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. linear moves at one constant speed. ease-in starts slow and finishes fast; ease-out starts fast and slows to a stop; ease-in-out is 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: ease is cubic-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, and linear(0, 0.8, 1) (Baseline 2023) draws an easing from a list of points, which can describe springs and bounces.

  4. 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, transform and shadows all interpolate: halfway from 100px to 200px is 150px. Some values have no "in between": display: none to block, visibility, font-family, or a length to the keyword auto (height: 0 to height: 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) lets display and 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. 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.offsetWidth first (this forces a style recalculation), then change the class; or use @starting-style.

  6. 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) and transitioncancel (it was stopped, for example because the property changed again or the element got display: none). Each event has propertyName and elapsedTime (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

01

What is a CSS transition, and what are its four parts?

02

Why should a hover transition be declared on the element's normal state rather than on :hover?

03

Which properties can be transitioned, and why can't height: auto or display transition by default?

04

Why does a transition sometimes not run when you add a class from JavaScript, and how do you fix it?

05

What do transitionrun, transitionstart, transitionend and transitioncancel mean?

Practice

01

Make a link whose underline colour fades from transparent to crimson over 0.25s when hovered, and fades back when the mouse leaves.

02

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.

03

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-size is 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 auto and display don't by default.

  • I can make a transition run from JavaScript by forcing a style calculation or using @starting-style.

  • I can listen for transitionend and know when it won't fire.

  • I can pick an easing (ease-out, cubic-bezier, steps, linear()) for a purpose.