An Interactive Guide to CSS Transitions

Craft high-performance, responsive UI motion with CSS transitions and transforms
Published at:
Last updated:
Estimated reading time: 14 min read

Introduction#

CSS transitions allow you to animate property changes smoothly over a duration — turning sudden visual changes into fluid, natural motion. Unlike keyframe animations, transitions are state-driven: the browser animates a property from one value to another automatically when that value changes. They are perfect for UI enhancements like hover effects, color fades, button presses, and expanding menus.

What Are CSS Transitions?#

A transition animates the change between two states of a property. When a property’s value changes (like background-color, width, or opacity), the browser interpolates between old and new values over the time.

Code language: CSS
1
2
3
4
5
6
7
8
.element {
  background-color: #007bff;
  transition: background-color 0.3s ease;
}

.element:hover {
  background-color: #0056b3;
}

Here, when you hover, the background color changes smoothly over 0.3s instead of instantly.

How Transitions Work#

A transition only occurs when a property changes — usually due to:

  • Pseudo-classes (:hover, :focus, :active)

  • Class toggling via JavaScript

  • Inline style changes (e.g., element.style.height = "200px")

  • DOM manipulation or layout updates

Code language: JavaScript
1
2
3
4
const box = document.querySelector(".box");
box.addEventListener("click", () => {
  box.classList.toggle("slide-in");
});
Code language: CSS
1
2
3
4
5
6
7
8
9
.box {
  width: 100px;
  height: 100px;
  transition: transform 0.3s ease-in-out;
}

.box.slide-in {
  transform: translateX(-4px);
}

Transition Properties#

CSS provides a set of transition-* properties to control transitions:

1. transition-property#

The transition-property CSS property specifies which CSS properties should transition.

Code language: CSS
1
2
3
.element {
  transition-property: opacity;
}
  • You can list multiple properties separated by commas.

  • Use all to animate every animatable property (not recommended for performance reasons).

  • none disables the transitions.

Tip: Animating all can be expensive for performance. For high-performance transitions, focus on properties that do not affect the layout (known as non-reflow properties), primarily opacity and transform (e.g., translate, scale).

2. transition-duration#

The transition-duration CSS property defines how long the transition takes to complete. The duration can be specified either in seconds (s) or milliseconds (ms).

Code language: CSS
1
2
3
4
5
6
.element {
  transition-property: opacity;

  /* Duration */
  transition-duration: 0.3s;
}

You can specify multiple durations for multiple properties, maintaining the order defined in transition-property property:

Code language: CSS
1
2
3
4
5
6
.element {
  transition-property: opacity, transform;

  /* Matches the order: opacity gets 0.3s, transform gets 0.6s */
  transition-duration: 0.3s, 0.6s;
}

3. transition-timing-function#

The transition-timing-function CSS property controls the speed curve of the transition — essentially, how the intermediate states are calculated over time.

Code language: CSS
1
2
3
4
5
6
7
.element {
  transition-property: opacity;
  transition-duration: 0.3s;

  /* Timing function */
  transition-timing-function: ease-in-out;
}

Common Timing Functions:

Function Description
linear Constant speed throughout.
ease Starts slow, speeds up, then slows down (default).
ease-in Starts slow, speeds up at end.
ease-out Starts fast, slows down at end.
ease-in-out Slow start and end, fast middle.
steps(n, start/end) Jump-based transitions for discrete steps.
cubic-bezier(x1, y1, x2, y2) Custom curve for precise control.

Analogy: Think of the transition as a car accelerating from a stoplight.

  • linear: The car instantly hits a constant speed and holds it. (Unnatural)

  • ease-in: The car starts slowly and then slams the gas at the end. (Hard start)

  • ease-out: The car accelerates quickly and then slowly coasts to its final stop. (Soft landing)

  • ease-in-out: The car accelerates smoothly and slows down smoothly, providing the most natural, polished feel.

You can specify multiple timing functions for multiple properties, maintaining the order defined in transition-property property:

Code language: CSS
1
2
3
4
5
6
7
.element {
  transition-property: opacity, transform;
  transition-duration: 0.3s, 0.6s;

  /* Matches the order: opacity gets ease-out, transform gets ease-in-out */
  transition-timing-function: ease-out, ease-in-out;
}

Note on cubic-bezier: This function gives you granular control over the curve, allowing you to simulate complex effects like a subtle spring or bounce (where the property briefly overshoots its final value) in your transitions.

4. transition-delay#

The transition-delay CSS property defines how long to wait before the transition starts.

Code language: CSS
1
2
3
4
5
6
7
8
.element {
  transition-property: opacity;
  transition-duration: 0.3s;
  transition-timing-function: ease-in-out;

  /* Delay */
  transition-delay: 0.2s;
}

This is useful for staggering multiple transitions.

You can specify multiple delays for multiple properties, maintaining the order defined in transition-property property:

Code language: CSS
1
2
3
4
5
6
7
8
.element {
  transition-property: opacity, transform;
  transition-duration: 0.3s, 0.6s;
  transition-timing-function: ease-out, ease-in-out;

  /* Matches the order: opacity gets 0s delay, transform gets 0.1s delay */
  transition-delay: 0s, 0.1s;
}

Negative Delays#

A negative value for transition-delay is perfectly valid. It tells the browser to start the transition immediately, but from a state corresponding to a point partway through its defined duration.

How It Works:

The negative value essentially “rewinds” the transition’s internal clock by that amount.

For example, applying transition-delay: -0.5s to a transition with a 1s duration means the transition starts instantly, but it visually skips the first half (0.5s) of its defined movement. The element appears at the midpoint of the transition’s change, and the remaining 0.5s of the transition will play out.

Key Use Cases:

  • Pre-Positioning The main power of a negative delay in transitions is pre-positioning elements at an intermediate state. Transitions, unlike animations, only define a start and end state (e.g., normal state and :hover state).

  • Pre-Setting the State: This is incredibly useful if you want an element to visually appear as if it is already partway through its transition when an event is triggered (like a hover or a class change).

    Scenario Code Result
    Normal State .element { transition: opacity 1s; transition-delay: 0s; } Transition starts at 0% opacity and fades in over 1 second.
    Pre-Positioned State .element { transition: opacity 1s; transition-delay: -0.5s; } Transition starts immediately, but visually at 50% opacity (the halfway point), and completes the remaining fade over 0.5 seconds.

This technique eliminates the need to define a separate intermediate CSS rule just to position an element slightly advanced in its transition when the page loads or a state change occurs.

5. transition#

Instead of writing multiple CSS transition properties separately, you can combine them into the transition shorthand property.

Code language: CSS
1
transition: <property> <duration> <timing-function> <delay>;

Example:

Code language: CSS
1
2
3
4
.element {
  /* Transition the 'opacity' property over 0.3s, using the ease-out curve, starting after a 0.1s delay. */
  transition: opacity 0.3s ease-out 0.1s;
}

You can also specify multiple transitions:

Code language: CSS
1
2
3
.element {
  transition: opacity 0.3s ease-out 0.1s, transform 0.6s ease-in-out 0s;
}

The Critical Order Rule: Duration vs. Delay

The order of the four components is flexible, except for the two time values (duration and delay):

  1. The first time value the browser reads is always the transition-duration.

  2. The second time value is always the transition-delay.

This means if you provide only one time value, the browser assumes it is the duration and the delay defaults to 0s.

Shorthand Interpretation
transition: opacity 1s ease; duration: 1s, delay: 0s
transition: opacity 0.5s linear 1s; duration: 0.5s, delay: 1s

Missing values (<property>, <timing-function>) will default to their initial states (all and ease, respectively).

Transition Events#

You can listen to events triggered during or after transitions:

Event Description
transitionrun Fired right before transition starts.
transitionstart Fired when a transition begins.
transitionend Fired after transition completes.
transitioncancel Fired if a transition is interrupted.

Example:

Code language: JavaScript
1
2
3
box.addEventListener("transitionend", () => {
  console.log("Transition completed!");
});

Debugging Transitions#

If transitions feel janky:

  • Check if layout properties (width, height) are being animated.

  • Use DevTools → Performance → “Paint” view to detect repaints.

  • Replace layout-affecting transitions with transform: scale() or translate().

The will-change property#

Modern browsers optimize animations using the will-change property. It hints to the browser which properties are likely to change, allowing pre-optimization like promoting elements to their own layer, reducing lag for expensive transitions.

Code language: CSS
1
2
3
.card {
  will-change: transform, opacity;
}

Use sparingly — excessive will-change declarations force the browser to immediately allocate memory and GPU resources (layer creation), which can increase memory usage and reduce performance across the rest of the page. Only apply will-change just before the transition is about to occur and remove it immediately after, if possible, to avoid continuous memory use.

Interactive Example#

Reading about timing functions is one thing, but feeling them is another. Use this interactive playground to experiment with different easing curves and durations. Notice how a slight change in the cubic-bezier coordinates can turn a robotic movement into a snappy, organic interaction.

Playground name: Demostrating CSS Transitions #

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<div class="playground-container">
<div class="demo-area">
<div id="animated-box" class="box-state-one"></div>
</div>

<div class="section-label">Transition Properties</div>

<div class="controls-grid">
<label for="duration-input">Duration (s):</label>
<input type="number" id="duration-input" min="0.1" max="10" step="0.1" value="3">

<label for="delay-input">Delay (s):</label>
<input type="number" id="delay-input" min="-10" max="10" step="0.1" value="0">
</div>

<div class="section-label">Timing Function</div>

<div class="timing-selector">
<label><input type="radio" name="timing-type" value="preset" checked> Preset</label>
<label><input type="radio" name="timing-type" value="bezier"> Cubic-Bezier</label>
<label><input type="radio" name="timing-type" value="steps"> Steps</label>
</div>

<div id="timing-options-container">

<div id="preset-options" class="timing-group active-group">
<label for="timing-select-preset">Select Preset:</label>
<select id="timing-select-preset">
<option value="ease">ease</option>
<option value="linear">linear</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
</select>
</div>

<div id="bezier-options" class="timing-group">
<label>x1:</label><input type="number" id="bezier-x1" min="-2" max="2" step="0.1" value="0.68">
<label>y1:</label><input type="number" id="bezier-y1" min="-2" max="2" step="0.1" value="-0.55">
<label>x2:</label><input type="number" id="bezier-x2" min="-2" max="2" step="0.1" value="0.27">
<label>y2:</label><input type="number" id="bezier-y2" min="-2" max="2" step="0.1" value="1.55">

<div class="tip">Try (0.1, 0.7, 1, 0.1) for a fast start.</div>
</div>

<div id="steps-options" class="timing-group">
<label for="steps-count">Steps Count:</label>
<input type="number" id="steps-count" min="-10" max="10" step="1" value="5">

<label for="steps-direction">Direction:</label>
<select id="steps-direction">
<option value="jump-end">jump-end (default)</option>
<option value="jump-start">jump-start</option>
<option value="jump-none">jump-none</option>
<option value="jump-both">jump-both</option>
<option value="start">start (classic)</option>
<option value="end">end (classic)</option>
</select>

<div class="tip">Use "start" to display first step immediately.</div>
</div>

</div>

<button id="toggle-state-btn" class="toggle-btn">Toggle Transition State</button>
</div>
This playground is under active development. Some features may be incomplete or behave unexpectedly. If you notice any bugs or unexpected behavior, please let me know!.

Best Practices#

Optimal Duration is Key: Aim for short durations, typically between 150–500ms, for user interface (UI) interactions. Transitions faster than 150ms can be missed, and those slower than 500ms can feel sluggish. 🐢

Prioritize Performance Properties (GPU-Accelerated): Avoid animating layout-affecting properties like width, height, top, or left. These trigger costly layout re-calculations (reflows). Instead, animate opacity or transform (e.g., scale, translate, rotate), as they run directly on the GPU (Graphics Processing Unit) and are significantly more performant.

Define Transitions in the Base State: Always declare the transition properties in the base selector (e.g., .element), not in the state selectors (:hover, .active). This ensures the transition applies both when entering the new state and when returning to the original state.

Leverage Shorthand and transform: Use the transition shorthand property for brevity and clarity. Always combine transitions with transform properties to create smooth, efficient motion that avoids taxing the main CPU thread.

Control the Exit: Use transition: none; (or explicitly set transition-duration: 0s;) when you need an instant visual update (e.g., hiding a pop-up or for accessibility toggles). This prevents unwanted animation on specific state changes.

Use will-change Judiciously: The will-change property should be used sparingly and briefly. Overuse forces the browser to prematurely allocate memory and GPU resources for every element, which can lead to increased memory usage and overall performance degradation across the page.

Conclusion#

CSS transitions are a lightweight, intuitive way to animate state changes. By using them correctly, particularly by leveraging GPU-accelerated properties like transform and opacity, you can ensure your UI motion is smooth, responsive, and performant. Understanding all transition-* properties, the nuances of timing functions, and strategically using will-change provides the control you need. Master them, and you’ll build a strong foundation for crafting delightful micro-interactions and tackling more advanced CSS animations.

While transitions are excellent for simple, state-driven changes, complex motion that requires more than two steps, looping, or scroll-linking necessitates CSS Keyframe Animations. For a full feature comparison, see the Keyframes vs. Transitions Comparison Table in my An Interactive Guide to CSS Keyframes post.

Key Takeaways:

  • Use them for micro-interactions (hover, focus, click)

  • Combine with transform for best performance

  • Optimize with will-change only when necessary

  • Keep motion subtle and consistent

I hope you found this post informative and helpful. It took a lot of work to create, and I’m thrilled to finally share it with the world. Thank you for reading. 💖

Page view counter

# of hits