1CSS Transitions
▶
CSS transitions are one of the most practical and widely-used features of modern CSS. They allow you to animate the change between two states of a CSS property smoothly, without writing a single line of JavaScript. When a property value changes — whether due to a user hovering over an element, an element receiving focus, or a class being toggled dynamically — the browser doesn't snap instantly to the new value. Instead, it interpolates between the old value and the new value over a defined period of time, producing a smooth, polished visual effect. Understanding CSS transitions deeply means understanding not just the individual properties, but how they interact, when to use them, and how to keep them performant.
What Are CSS Transitions?
At the most fundamental level, a CSS transition is a way of telling the browser: "When this property changes, don't change it immediately — animate it over time." Transitions are triggered by a change in a CSS property's computed value. The most common triggers include pseudo-class selectors like :hover or :focus, but transitions also fire when a class is added or removed via JavaScript, when an attribute changes, or even when a media query applies a different value.
A transition requires two things to be meaningful: a start state and an end state. The start state is the value the property holds before the trigger, and the end state is the value after the trigger. The browser then calculates all the intermediate values between those two states and renders them across the specified duration. For example, if a button's background color is blue in its resting state and green on hover, the browser will render dozens of intermediate color values between blue and green during the transition.
A critically important and often misunderstood rule is that transitions must be defined on the element in its default state, not on the triggering selector. Consider the following correct pattern:
/* Correct: transition defined on the base element */
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: green;
}
If you placed the transition declaration only on .button:hover, the animation would play when the cursor enters the element (as the hover state is applied), but would snap back instantly with no animation when the cursor leaves (because there is no transition defined on the base state that applies on the way back). By placing it on the base element, the transition is always present, animating the property change in both directions.
The transition-property
The transition-property property specifies which CSS property (or properties) should be transitioned. You can target a single property, a list of properties, or use the keyword all to animate every animatable property simultaneously.
/* Single property */
transition-property: opacity;
/* Multiple properties */
transition-property: opacity, transform, background-color;
/* All animatable properties — use with caution */
transition-property: all;
It is important to understand that not all CSS properties are animatable. Animatable properties are those whose values can be meaningfully interpolated — that is, the browser can calculate an in-between value. Numeric values like opacity, width, font-size, and color values like background-color or color are all animatable because there is a clear numerical path between any two values. Properties like display, visibility (partially), or font-family cannot be transitioned in a meaningful way because there is no numeric midpoint between, say, display: none and display: block. The browser simply switches them immediately, ignoring any transition instruction.
Using transition-property: all is convenient but risky. It means that any property change — even ones you didn't intend to animate, perhaps triggered by a JavaScript style change — will be animated. This can lead to unexpected visual behavior and unnecessary performance costs. It is almost always better to be explicit about which properties you are transitioning.
The transition shorthand property bundles transition-property, transition-duration, transition-timing-function, and transition-delay into a single concise declaration, which is how most developers write transitions in practice:
/* Shorthand */
transition: background-color 0.3s ease 0s;
Controlling Duration with transition-duration
The transition-duration property determines how long the transition animation takes to complete, from start state to end state. Duration values must always include a time unit. The two accepted units are seconds (s) and milliseconds (ms). A value of 0.3s and a value of 300ms are exactly equivalent — both represent 300 milliseconds. Omitting the unit will cause the declaration to be invalid and ignored by the browser.
/* These are equivalent */
transition-duration: 0.3s;
transition-duration: 300ms;
When you are transitioning multiple properties simultaneously using a comma-separated transition-property list, you can supply a corresponding comma-separated list of durations. The browser maps the duration values to the property values by their position in the list. If there are fewer durations than properties, the browser cycles back through the duration list from the beginning.
.card {
transition-property: opacity, transform, background-color;
transition-duration: 0.2s, 0.4s, 0.6s;
/* opacity: 200ms, transform: 400ms, background-color: 600ms */
}
Choosing the right duration is as much about user experience as it is about technical correctness. For small UI interactions like button hover effects or tooltip appearances, durations in the range of 150ms to 300ms tend to feel snappy and responsive. Durations shorter than 100ms may happen so fast that users don't perceive the animation at all, defeating the purpose. Conversely, durations longer than 400–500ms for simple UI feedback can feel sluggish and frustrating, making the interface feel unresponsive. Longer durations (500ms to 1s) are more appropriate for larger, more deliberate animations such as sliding panels or page-level transitions.
Timing Functions and transition-timing-function
Duration tells the browser how long a transition takes, but the transition-timing-function tells the browser how the property changes over that time. Does it change at a steady pace? Does it start fast and slow down? Does it ease in gently? These questions are all answered by the timing function. The timing function is mathematically represented as a cubic Bézier curve, and CSS provides several named keywords as convenient shortcuts, along with the ability to define a fully custom curve.
| Keyword / Function | Description | Typical Use Case |
|---|---|---|
ease |
Starts slow, accelerates, then decelerates at the end. The browser default. | General UI animations, hover effects — feels natural to humans. |
linear |
Constant rate of change from start to finish. | Progress bars, rotating loaders, anything requiring uniform motion. |
ease-in |
Starts slowly, then accelerates toward the end. | Elements leaving the screen (the slow start feels intentional, the quick exit feels decisive). |
ease-out |
Starts fast, then decelerates at the end. | Elements entering the screen (quick appearance, gentle landing). |
ease-in-out |
Slow start, fast middle, slow end. Similar to ease but more symmetrical. | Elements that move from one position to another within the viewport. |
steps(n, direction) |
Divides the animation into n discrete steps rather than a smooth curve. | Sprite-sheet animations, typewriter effects, frame-by-frame motion. |
cubic-bezier(x1,y1,x2,y2) |
Fully custom easing curve defined by two control points. | Any animation requiring a precise, branded, or unconventional feel. |
The ease keyword is equivalent to cubic-bezier(0.25, 0.1, 0.25, 1.0) and is the default timing function if none is specified. It mimics the natural acceleration and deceleration of physical objects, which is why it feels intuitively correct for most UI animations.
linear is equivalent to cubic-bezier(0, 0, 1, 1). Because it changes at a perfectly constant rate, it can feel mechanical and unnatural for most animations, but it is exactly right for things like a spinning loading indicator or a progress bar where you want to communicate a steady, predictable process.
The steps() function is unique because it creates a discrete transition rather than a smooth one. steps(4, end), for instance, divides the animation into exactly four instantaneous jumps, each occurring at the end of each step interval. This is commonly used to animate sprite sheets where a series of individual frames need to be shown one after another:
.sprite {
background-position: 0 0;
transition: background-position 0.8s steps(8, end);
}
.sprite:hover {
background-position: -800px 0;
}
The cubic-bezier() function provides the highest degree of control. It accepts four numbers representing the x and y coordinates of two control points (P1 and P2) on a unit grid. The start point (P0) is always (0,0) and the end point (P3) is always (1,1). Tools like the Chrome DevTools cubic-bezier editor or the website cubic-bezier.com allow you to visually design and copy your custom curve:
/* A custom "bounce-like" feel */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
Adding a Delay with transition-delay
The transition-delay property specifies how long the browser should wait after the triggering event before beginning the transition animation. Like transition-duration, delay values are expressed in seconds or milliseconds, and you can provide a comma-separated list to assign different delays to each property in a multi-property transition.
.menu-item {
transition-property: opacity, transform;
transition-duration: 0.3s, 0.3s;
transition-delay: 0s, 0.1s;
/* opacity starts immediately; transform starts 100ms later */
}
Staggered delays like the example above are a powerful technique for creating cascading animation effects, where multiple elements or properties animate sequentially rather than all at once, giving a much more polished and dynamic feel to a UI interaction.
One of the more surprising features of transition-delay is that it can accept negative values. A negative delay does not mean the transition starts before the trigger — that is impossible. Instead, a negative delay causes the transition to begin immediately but appear as if it has already been playing for the absolute value of the delay. For example, transition-delay: -0.2s on a 0.5s transition means the animation starts immediately but skips the first 200ms of the animation, jumping straight to the 40% mark and playing from there:
.element {
transition: transform 0.5s ease -0.2s;
/* Starts immediately, but begins at 40% through the animation */
}
This technique is occasionally useful for fine-tuning the feel of complex sequences or for creating effects where an element appears to be "already in motion" when it appears.
The Transition Shorthand Property
Rather than declaring all four transition sub-properties separately, the transition shorthand allows you to write them in a single line. The canonical order for the shorthand is:
transition: [property] [duration] [timing-function] [delay];
For example:
.button {
transition: background-color 0.3s ease 0s;
}
A key parsing rule to remember is that when two time values are present in a single transition definition, the first time value is always interpreted as the duration and the second as the delay. This is because both values share the same syntax (a number followed by a time unit), so the browser uses positional order to distinguish them:
/* Duration: 0.4s, Delay: 0.1s */
transition: opacity 0.4s ease 0.1s;
If you only specify one time value, it is treated as the duration, and the delay defaults to 0s.
Multiple independent transitions can be combined in a single transition declaration by separating each complete transition definition with a comma. This is the most common and readable approach in production CSS:
.card {
transition:
opacity 0.3s ease,
transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
box-shadow 0.3s ease 0.05s;
}
In this example, opacity and box-shadow animate over 300ms with ease, while transform uses a custom cubic-bezier curve over 400ms, and box-shadow additionally has a 50ms delay before it begins. This level of specificity allows developers to craft genuinely refined animations with a single concise CSS declaration.
Performance Considerations for CSS Transitions
Not all CSS transitions are created equal from a performance standpoint. Understanding how browsers render pages is essential to writing transitions that stay smooth, ideally maintaining a consistent 60 frames per second (fps), which means each frame must be rendered in under approximately 16.67 milliseconds.
The browser rendering pipeline has several stages: Style (calculating which CSS rules apply), Layout (calculating the size and position of every element), Paint (filling in the pixels), and Composite (layering the painted results together). When you animate a property that affects the geometry of elements — such as width, height, padding, margin, or top/left — the browser must run through the entire Layout stage on every single frame of the animation. This is called reflow, and it is computationally expensive, especially on pages with complex DOM structures where changing one element's layout can cascade through many others.
Similarly, animating properties like background-color, color, or border requires the browser to repaint the affected area every frame — also expensive, though generally less so than reflow.
The two properties that can be animated at the Composite stage only — completely bypassing Layout and Paint — are transform and opacity. When you move an element using transform: translateX() instead of changing its left property, the browser can hand that work off to the GPU and perform the calculation almost entirely outside the main thread. This is why performance-conscious developers use transform for all movement and scaling animations, and opacity for all fade effects:
/* ❌ Causes reflow on every frame — avoid for animation */
.box {
transition: width 0.3s ease, margin-left 0.3s ease;
}
/* ✅ Compositor-only — stays smooth at 60fps */
.box {
transition: transform 0.3s ease, opacity 0.3s ease;
}
The will-change property is an additional performance optimization hint that tells the browser to prepare for an upcoming animation before it starts. By declaring will-change: transform or will-change: opacity, the browser can promote the element to its own compositor layer in advance, eliminating the latency of doing so mid-animation:
.animated-element {
will-change: transform, opacity;
}
However, will-change should be used sparingly and deliberately. Promoting every element to its own compositor layer consumes additional GPU memory. Applying it to many elements simultaneously, or applying it globally via a wildcard selector, can actually degrade performance by exhausting available memory. The best practice is to apply will-change only to elements that are genuinely about to be animated — sometimes added and removed via JavaScript just before and after an animation runs — and to specific high-impact elements like modal dialogs, dropdown menus, or image carousels that animate frequently.
| Property Animated | Pipeline Stage Triggered | Performance Impact |
|---|---|---|
width, height, margin, padding, top, left |
Layout (Reflow) → Paint → Composite | High — avoid for smooth animations |
background-color, color, border-color, box-shadow |
Paint → Composite | Medium — acceptable for simple effects |
transform, opacity |
Composite only | Low — preferred for all performance-sensitive animations |
By combining a solid understanding of the transition sub-properties with thoughtful choices about which properties to animate, developers can create interfaces that feel polished, responsive, and professional — without sacrificing the smooth experience that users expect from modern web applications.