CSS Transitions
Animation on the web used to mean JavaScript timers, Flash plugins, or heavy libraries. CSS transitions changed that entirely. With a single property, you can tell the browser to smoothly interpolate between two states, handling all the frame-by-frame math automatically and doing it on the GPU when possible. The result is silky motion that would have taken hundreds of lines of JavaScript to recreate a decade ago.
Transitions are the simplest form of CSS animation: they respond to state changes. When an element moves from one state to another (hover, focus, a class being toggled), a transition defines how that change happens over time instead of cutting immediately. You will build on this foundation throughout the unit as you move into keyframe animations and interactive components.
The Four Transition Properties
CSS transitions are controlled by four individual properties that you can set separately or combine into the transition shorthand. Understanding each one individually before using shorthand will prevent a lot of confusion.
transition-property
Specifies which CSS property (or properties) should be animated. You can target a single property, a comma-separated list, or use the keyword all to animate every property that changes simultaneously.
/* Animate only the background color */
transition-property: background-color;
/* Animate both color and transform */
transition-property: color, transform;
/* Animate everything that changes */
transition-property: all;
Using all is convenient during development but dangerous in production code. It will animate every property that changes on the element, including ones you didn't intend, and can trigger expensive repaints on properties that are not GPU-accelerated. Always be explicit about what you want to transition.
transition-duration
Sets how long the transition takes to complete. Values are in seconds (s) or milliseconds (ms). There is no default duration, which means a transition with no duration set will be instantaneous. For UI interactions, durations between 150ms and 400ms generally feel natural without being sluggish.
transition-duration: 300ms;
transition-duration: 0.3s; /* Same thing */
transition-duration: 200ms, 500ms; /* Different durations per property */
transition-timing-function
Controls the acceleration curve of the transition, determining how the animated value progresses through time. This is what separates mechanical-feeling motion from motion that feels physical and alive. The default value is ease. You will explore the full range of options in the next section.
transition-delay
Adds a pause before the transition begins. This is useful for sequencing multiple transitions or for giving users a moment before an animation fires. Like duration, values are in seconds or milliseconds.
/* Wait 100ms before the transition starts */
transition-delay: 100ms;
/* Negative delay: start the transition partway through */
transition-delay: -200ms;
A negative delay is a lesser-known trick: it starts the transition as if it had already been running for that amount of time, which lets you begin an animation mid-motion rather than from the very start.
The Shorthand
The transition shorthand combines all four properties in order: property, duration, timing-function, delay. When specifying multiple transitions, separate them with commas.
/* Single transition */
transition: background-color 300ms ease 0ms;
/* Multiple transitions on the same element */
transition:
background-color 300ms ease,
transform 400ms ease-out,
opacity 200ms linear;
When writing multiple transitions in the shorthand, put each one on its own line as shown above. This makes the code far easier to scan and edit, and follows the same principle as writing complex selectors or grid templates across multiple lines.
Timing Functions
Timing functions are arguably the most important part of a transition. Two transitions with identical durations can feel completely different depending on their timing function. The named keywords are convenient aliases for specific cubic-bezier curves, which are the underlying mathematical model for all CSS easing.
Named Keywords
CSS provides five named timing functions that cover the most common motion patterns:
-
ease — Starts slow, accelerates through the middle, then decelerates at the end. This is the default and works well for most UI transitions. Equivalent to
cubic-bezier(0.25, 0.1, 0.25, 1.0). - linear — Constant speed throughout. Rarely feels natural for UI motion because nothing in the physical world moves at perfectly constant velocity. Useful for looping animations or progress bars where you want predictable, mechanical motion.
-
ease-in — Starts slow and accelerates. Good for elements leaving the screen, since things that are departing should feel like they are gaining momentum. Equivalent to
cubic-bezier(0.42, 0, 1.0, 1.0). -
ease-out — Starts fast and decelerates. Good for elements entering the screen, since things arriving should feel like they are slowing down as they land. Equivalent to
cubic-bezier(0, 0, 0.58, 1.0). -
ease-in-out — Slow start and slow end with faster motion in the middle. More symmetrical than
ease. Good for elements that move from one visible position to another. Equivalent tocubic-bezier(0.42, 0, 0.58, 1.0).
Cubic-Bezier Functions
All of the named keywords above are just shortcuts for specific cubic-bezier() values. A cubic bezier curve is defined by four numbers: two control points that shape the curve between a fixed start point (0,0) and end point (1,1). The horizontal axis represents time (0 = start, 1 = end) and the vertical axis represents progress through the animated value.
/* Custom easing that overshoots slightly before settling */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Strong ease-in for dramatic departures */
transition-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
/* Snappy settle: fast start, very gradual finish */
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
The control point values are not restricted to the 0–1 range. Values outside that range create curves that overshoot or undershoot the target value, which is how you achieve bounce-like effects purely through CSS easing rather than keyframes. Browser DevTools include a cubic-bezier editor that lets you drag the handles visually and see the curve in real time, which is the most practical way to dial in custom easing.
Steps
The steps() function creates discrete, jumping transitions instead of smooth interpolation. You specify a number of steps and an optional direction. This is useful for sprite sheet animations or intentionally mechanical motion.
/* Jump in 4 discrete steps */
transition-timing-function: steps(4, end);
/* Named aliases */
transition-timing-function: step-start; /* steps(1, start) */
transition-timing-function: step-end; /* steps(1, end) */
Timing Function Demo
The demo below shows all five named timing functions side by side. Click the button to trigger the transition and observe how each curve affects the feel of the same 600ms duration.
Transitions with Transforms
Transforms from Unit 4 — translate, rotate, and scale — are among the most valuable properties to transition. They are handled by the compositor thread, meaning the browser can animate them without triggering layout or paint. This makes them highly performant compared to animating properties like top, left, width, or margin.
The pattern is straightforward: define the transform you want on a state change (hover, focus, a toggled class), then place the transition on the element itself rather than on the state selector. This ensures the transition fires in both directions — when entering the state and when leaving it.
.card {
/* Transition lives on the element, not the hover state */
transition: transform 300ms ease-out;
}
.card:hover {
transform: translateY(-4px);
}
If you put transition only on :hover, the animation plays when hovering but cuts immediately when you move the cursor away. Placing the transition on the base element ensures smooth motion in both directions.
Combining Multiple Transforms
Multiple transforms can be chained on a single property. The order matters: transforms are applied right to left, so translate rotate scale means scale first, then rotate, then translate.
.icon:hover {
/* Lift, rotate slightly, and grow */
transform: translateY(-6px) rotate(15deg) scale(1.05);
}
When combining transforms in a transition, you only need a single transition: transform declaration since all transforms are on the same property.
Transform Demo
Hover over each card below to see how translate, rotate, scale, and a combined transform each feel with a 400ms ease-out timing function.
Common Transition Patterns
Transitions show up in nearly every UI component. Understanding a handful of reliable patterns will cover the vast majority of what you will encounter in practice.
Hover Lift and Shadow
A classic card interaction: combine translateY with a box-shadow transition to create the illusion of lifting off the page. Transitioning both properties together creates a physically convincing effect.
.card {
transition:
transform 250ms ease-out,
box-shadow 250ms ease-out;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
Opacity Fade
Fading elements in and out is one of the most common UI patterns. opacity is GPU-accelerated and transitions smoothly. Combine it with visibility when you need the element to stop receiving pointer events while invisible — display: none cannot be transitioned, but visibility can with a delay trick.
.tooltip {
opacity: 0;
visibility: hidden;
transition:
opacity 200ms ease,
visibility 0s linear 200ms; /* Delay visibility change until fade completes */
}
.tooltip.visible {
opacity: 1;
visibility: visible;
transition:
opacity 200ms ease,
visibility 0s linear 0ms; /* No delay when showing */
}
Color Transitions
Background color, text color, and border color all transition smoothly. This is commonly used on buttons and links for feedback. Keep these transitions short (150–200ms) so the UI feels immediately responsive.
.btn {
background-color: #3b82f6;
transition: background-color 150ms ease;
}
.btn:hover {
background-color: #2563eb;
}
Width and Height Transitions
Transitioning width and height works but triggers layout recalculation, making it more expensive than transform-based animations. For expanding/collapsing elements, the most common technique uses max-height transitioning from 0 to a value large enough to contain the content. A cleaner modern alternative is transitioning grid-template-rows from 0fr to 1fr, which avoids layout thrashing in many cases.
/* Modern grid approach for collapsible content */
.collapsible-wrapper {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms ease;
}
.collapsible-wrapper.open {
grid-template-rows: 1fr;
}
.collapsible-wrapper > div {
overflow: hidden;
}
Focus Styles
Transitions are not just for hover. Applying them to :focus-visible states creates polished keyboard navigation experiences. Transitioning outline-offset or box-shadow on focus gives users a clear visual signal that responds smoothly to their input.
.btn {
outline: 2px solid transparent;
outline-offset: 0;
transition:
background-color 150ms ease,
outline-offset 150ms ease;
}
.btn:focus-visible {
outline-color: #3b82f6;
outline-offset: 3px;
}
Performance Considerations
CSS transitions are generally well-optimized by browsers, but there are specific properties that perform significantly better than others. Understanding why helps you make deliberate decisions rather than guessing.
The browser rendering pipeline has four stages: Style → Layout → Paint → Composite. When a CSS property changes, it may trigger the pipeline starting from different stages. Properties that only require compositing are the fastest because they skip layout and paint entirely. The two properties in this category are transform and opacity. Animating anything else will trigger layout (also called reflow) or paint (also called repaint), which is more expensive.
/* Expensive: triggers layout on every frame */
.slow { transition: width 300ms ease; }
.slow { transition: margin 300ms ease; }
.slow { transition: top 300ms ease; }
/* Fast: compositor only, no layout or paint */
.fast { transition: transform 300ms ease; }
.fast { transition: opacity 300ms ease; }
In practice, use transform: translate() instead of animating top/left, use transform: scale() instead of animating width/height, and use opacity for fades. These swaps have a massive impact on scroll performance and on lower-powered devices.
The will-change property hints to the browser that an element is about to be animated, allowing it to promote the element to its own compositor layer in advance. Use it sparingly and only when you have measured a performance problem. Overusing will-change consumes GPU memory and can make performance worse. Apply it only immediately before an animation is expected and remove it afterward when possible.
Accessibility and Reduced Motion
Motion on screen is not neutral. For users with vestibular disorders, migraines, or motion sensitivity, animated interfaces can cause real physical discomfort. The prefers-reduced-motion media query gives users a way to express this preference, and it is your responsibility to respect it.
.card {
transition: transform 300ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
A more maintainable pattern wraps all motion declarations together so you do not have to hunt down every transition across your stylesheet:
@media (prefers-reduced-motion: no-preference) {
.card { transition: transform 300ms ease-out; }
.btn { transition: background-color 150ms ease; }
}
This inverted approach means motion is opt-in based on the user's system setting rather than opt-out. Either approach works; the key principle is that transitions and animations should degrade gracefully to an instant state change when the user has indicated they prefer reduced motion. The interface should still function completely without any animation at all.
Skipping reduced motion support is not just a polish issue — it is an accessibility failure. Every transition you write should have a corresponding prefers-reduced-motion consideration before the component is considered complete.