This article is part of a series dedicated to Web Animation.
- Web Animation
- CSS Transitions
- CSS Animations
- SVG Animations (Coming soon)
- JavaScript Animations (Coming soon)
- Canvas Animations (Coming soon)
Here you’ll find a detailed theoretical explanation of CSS Transitions. If you want to see implementation examples and use cases, check out the video.
CSS property state changes
Normally, when we change the value of one or more CSS properties, the result rendered by the browser updates instantly. We can see this in the following example, where the width and box-shadow values change immediately when hovering over the logo, or on tap if you’re on a touch device.
Transitions are a presentation effect. The browser’s rendering engine performs an interpolation, calculating the intermediate values between one value and another.
Here we have the same example applying the transition.
How transitions work
If we have an element on our page that we want to move 200px to the right, we can do it with the transform property.
In the example we’ll use a bit of JavaScript to add interaction, in this case we’ll add a class to indicate the element’s new state. We could do this without JavaScript, with a checkbox and CSS selector combinations, but I believe JavaScript helps us add user interaction. But if there’s interest, we’ll see in a future CSS article how to do these things without JavaScript.
As we’ve seen, the transition property tells the browser to perform interpolation. The browser will attempt, depending on available resources, to update page rendering at 60 frames per second.
If we consider that the displacement is 200px in 0.5 seconds, in an ideal case of 60 frames/second, the interpolation will be 30 frames. The browser will calculate for interpolation: 200px/30fps = 6.666px, so it will move the element 6.666px to the right in each animation cycle.
In the following image we can see Chrome’s Developer Tools representation of the transition.

We have a graphical representation of the number of frames on the timeline, and we can also see the time it took to complete the transition 502.29 ms.
In developer tools, both Chrome and Firefox provide a lot of information and tools. We’ll analyze them in detail in a future article, and learn how we can use them to debug our animations or analyze animations on other websites (you learn a lot by analyzing other people’s development).
CSS properties for transitions
Let’s analyze in detail each of the CSS properties we have to define and control transitions.
transition-property
In this property we can define the CSS property we want to apply the transition to.
If we want to apply a transition between background color changes of an element, we should define it as follows: transition-property: background-color.
If we look at the W3C documentation we’ll see the following table.
| Values | none OR [ all OR <property-name> ]# |
| Initial value | all |
| Applies to | All elements and :before and :after pseudo-elements |
| Computed value | As specified |
| Inherited | No |
| Animatable | No |
We’ll see this table for almost all CSS properties in the official W3C documentation. It informs us of the possible values we can define for that property, the initial value it has if we don’t define one, the elements it applies to, the computed value, whether it inherits the value from the ancestor element, and whether it’s an animatable value.
A notable value from this information is the initial value, all, which means we can omit defining the property we want to animate. Another interesting piece of information is the hashtag # at the end of the values, indicating that the property accepts a comma-separated list of values.
So we could define the following to reference multiple properties.
.logo {
transition-property: width, box-shadow;
}
I’ll dedicate an article to animation performance, and we’ll analyze whether explicitly defining the properties we want to animate has an impact compared to leaving the default
allvalue.
transition-duration
With the transition-duration property we define the transition time, or transitions if we define a comma-separated list (here we can also see the #). It accepts positive values with units of seconds (s) or milliseconds (ms).
| Values | <time># |
| Initial value | 0s |
| Applies to | All elements and :before and :after pseudo-elements |
| Computed value | As specified |
| Inherited | No |
| Animatable | No |
If we define a negative value, the browser ignores the rule and changes would apply without any transition.

transition-timing-function
With the transition-timing-function property we have the option to define timing functions. These are a series of values: ease, linear, ease-in, ease-out, ease-in-out, step-start, step-end, steps(n, start) predefined from Bézier Curves cubic-bezier(x1, y1, x2, y2).
Bézier Curves deserve a specific and detailed article, as we have a specification dedicated to their definition, CSS Easing Functions Level 1, so we’ll cover it in future articles analyzing the technical aspects and implementation use cases.
Here we have the table with basic information.
| Values | <timing-function># |
| Initial value | ease |
| Applies to | All elements and :before and :after pseudo-elements |
| Computed value | As specified |
| Inherited | No |
| Animatable | No |
We can see that the default value is ease which is equivalent to the Bézier Curve cubic-bezier(0.25, 0.1, 0.25, 1).

This is its graphical representation. As I mentioned, we’ll see all of them in more detail in a future article.
transition-delay
As its name indicates, the transition-delay property allows us to define a value to indicate the wait time before starting the transition.
The value table is identical to transition-duration.
| Values | <time># |
| Initial value | 0s |
| Applies to | All elements and :before and :after pseudo-elements |
| Computed value | As specified |
| Inherited | No |
| Animatable | No |
This property does allow defining a negative value. For example, if we have a value of -250ms in a transition with a duration of 500ms, the animation will have half the journey. And if the negative value is greater than its positive transition-duration equivalent, we won’t get a transition, but an immediate change between the two states.
Here we can see an example of defining a negative value in the transition-delay property.
transition (shorthand)
Many CSS properties have a shorthand (abbreviated form). It allows us to use one property with multiple values. Let’s see an example that I think is much easier to understand.
/* Transition with multiple properties */
.logo {
transition-property: width;
transition-duration: 0.5s;
transition-timing-function: ease-out;
transition-delay: 0.25s;
}
/* Transition with shorthand */
.logoJL {
transition: width 0.5s ease-out 0.25s;
}
In the code above, both classes would apply the same transition. When we define a transition as shorthand in our code, if we inspect it with DevTools we can see it like this.

We can see there’s a triangle, indicating there’s more information available in that CSS property. If we click on it, we can observe that internally the browser interprets them separately.

As with previous properties, in transition we also have the informative table from the specification.
| Values | <single-transition># |
| Initial value | all 0s ease 0s |
| Applies to | All elements and :before and :after pseudo-elements |
| Computed value | As specified |
| Inherited | No |
| Animatable | No |
<single-transition> = [ [ none ‖ <transition-property> ] ‖ <time> ‖ <transition-timingfunction> ‖ <time> ]#
In the examples we’ve been seeing, I’ve been using the transition as shorthand, as it’s the most common approach.
Working with transitions
Default values
When working with transitions, we need to understand how the browser interprets the syntax.
The default value of transition-property or as a value in the shorthand transition is all, which means the transition we apply will affect all properties (that allow it) of the CSS class where we’re indicating it.
.logo {
transition-property: all;
transition-duration: 250ms;
transition-timing-function: ease;
}
.logoJL {
transition: all 250ms ease;
}
In the code above, both classes do the same thing, but there’s a detail we should consider. If we look at what the default values are for each of these properties, we can see we’re defining values that the browser already has by default.
.logo {
transition-duration: 250ms;
}
.logoJL {
transition: 250ms;
}
This code will have exactly the same behavior, even though we haven’t defined values for transition-property and transition-timing-function.
For semantic reasons and later code interpretation, whether by ourselves in the future or by another person, it’s common to (at least) find the
transition-propertyvalue defined.
Multiple transitions
We’ve already seen this from the first example in the article.
.logo {
... transition: width 0.25s, box-shadow 0.25s;
...;
}
We’ve defined transitions independently for width and box-shadow. In this case we could have done it with a value of all .25s, it would have the same effect. But doing it separately allows us to define different times, delays, or timing functions for each property.
.logo {
... transition-property: width, box-shadow;
transition-duration: 0.25s, 250ms;
transition-timing-function: ease, ease-in-out;
...;
}
.logoJL {
... transition: width 0.25s, box-shadow 0.25s ease-in-out;
...;
}
As an example I’ve used units in seconds and milliseconds, to show they can coexist without problems.
In case we define multiple properties, where the number of properties is greater than the number of values in the duration property, it repeats.
In the following code, the values of transition-duration and transition-timing-function will repeat until they match the number of values in transition-property.
.logo {
... transition-property: width, box-shadow, color, padding, border-radius;
transition-duration: 0.25s, 250ms;
transition-timing-function: ease, ease-in-out;
...;
}
Resulting in:
.logo {
... transition-property: width, box-shadow, color, padding, border-radius;
transition-duration: 0.25s, 250ms, 0.25s, 250ms, 0.25s;
transition-timing-function: ease, ease-in-out, ease, ease-in-out, ease;
...;
}

Value order
As the W3C documentation itself indicates, the order of values in the transition property is important. As we’ve seen in the tables above, it tells us <single-transition> = [ none | <single-transition-property> ] || <time> || <timing-function> || <time>. This means the browser expects: none or a property, followed by duration, timing function, and finally delay time.
I’ve been testing various combinations, the browser is intelligent enough to correct errors in order.
.logo {
transition: ease-in opacity 250ms 3s;
}
In the code above, the browser will be able to interpret the transition even though the values aren’t ordered following the standard. Even if the first time value is negative, it interprets we’re indicating the transition.delay value since we can’t have a negative value in transition-duration. I invite you to try combinations to understand how it works.
How do transitions work internally?
Beyond knowing how to define our transitions to add animations and interactions, we can look at browser code to see how they’re applying what we can see in the W3C specifications, which in fact is documentation for browser developers, let’s be clear.
We’re fortunate to have the source code of most browsers available.
- Chromium the engine used in Google Chrome, Opera and recently by Microsoft’s browser
- Gecko the engine used by Mozilla for Firefox
- WebKit was Chrome and Opera’s engine, but now only Safari uses it
It’s interesting to see code like the following, where we can see exactly how it creates the list of values for a shorthand transition.
const CSSValue* Transition::CSSValueFromComputedStyleInternal(
const ComputedStyle& style,
const SVGComputedStyle&,
const LayoutObject*,
bool allow_visited_style) const {
const CSSTransitionData* transition_data = style.Transitions();
if (transition_data) {
CSSValueList* transitions_list = CSSValueList::CreateCommaSeparated();
for (wtf_size_t i = 0; i < transition_data->PropertyList().size(); ++i) {
CSSValueList* list = CSSValueList::CreateSpaceSeparated();
list->Append(*ComputedStyleUtils::CreateTransitionPropertyValue(
transition_data->PropertyList()[i]));
list->Append(*CSSNumericLiteralValue::Create(
CSSTimingData::GetRepeated(transition_data->DurationList(), i),
CSSPrimitiveValue::UnitType::kSeconds));
list->Append(*ComputedStyleUtils::CreateTimingFunctionValue(
CSSTimingData::GetRepeated(transition_data->TimingFunctionList(), i)
.get()));
list->Append(*CSSNumericLiteralValue::Create(
CSSTimingData::GetRepeated(transition_data->DelayList(), i),
CSSPrimitiveValue::UnitType::kSeconds));
transitions_list->Append(*list);
}
return transitions_list;
}
CSSValueList* list = CSSValueList::CreateSpaceSeparated();
// transition-property default value.
list->Append(*CSSIdentifierValue::Create(CSSValueID::kAll));
list->Append(
*CSSNumericLiteralValue::Create(CSSTransitionData::InitialDuration(),
CSSPrimitiveValue::UnitType::kSeconds));
list->Append(*ComputedStyleUtils::CreateTimingFunctionValue(
CSSTransitionData::InitialTimingFunction().get()));
list->Append(
*CSSNumericLiteralValue::Create(CSSTransitionData::InitialDelay(),
CSSPrimitiveValue::UnitType::kSeconds));
return list;
}
Conclusions
Transitions were the first step to having animations on the web, they’re as basic as they are powerful. They allow us to define highly optimized animations. By combining multiple properties, duration times, and delay times, we can create small animations. But when we need to create animations with more than two states, we need to use the animation properties along with @keyframes, but we’ll see that in the next article in this series.