CSS Transitions: How to Animate Changes on a Web Page
Learn how CSS transitions interpolate changes in a web page, with runnable examples, timing options, reduced-motion support, debugging tips, and newer entry and exit patterns.
A CSS transition makes a change in a CSS property happen gradually over time. Put the transition on the element’s base rule, then change the property value in a state such as :hover, :focus-visible, or a class added by JavaScript. The transition animates the change; it does not create the state change.
Here is a complete hover and keyboard-focus example:
<button class="button" type="button">Save changes</button>
.button {
background-color: #2457c5;
color: white;
border: 0;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
transform: translateY(0);
transition: background-color 180ms ease, transform 180ms ease;
}
.button:hover,
.button:focus-visible {
background-color: #17449f;
transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
.button {
transition: none;
}
}
The base rule declares which properties transition. The state rule provides their destination values. The example’s duration and easing are illustrative; choose values that fit the interface and review the motion in context.
1. How CSS transitions work
When a property’s computed value changes, the browser can interpolate between its old and new values over a specified duration. A transition may be triggered by a pseudo-class, a class change, or another change in CSS state. Without a changed value, there is nothing to animate. The transition declaration is usually placed in the base rule so it applies both when the state begins and when it returns.
For example, moving the pointer onto a button changes its background and transform. Moving the pointer away changes those values back, and the same transition declaration animates the return.
2. The transition shorthand and its options
The shorthand is transition: property duration timing-function delay behavior;. The first four parts are the familiar controls; the optional behavior controls how discrete values are handled.
| Control | What it does | Example |
|---|---|---|
transition-property |
Selects which CSS properties to animate. Listing properties explicitly makes the intended motion clear. | opacity, transform |
transition-duration |
Sets how long the change takes. Its initial value is 0s, so a nonzero duration is needed for a visible transition. |
180ms |
transition-timing-function |
Controls the progress of intermediate values over time. | ease, linear, cubic-bezier(.2,.8,.2,1) |
transition-delay |
Waits this long after the value changes before starting. | 80ms |
transition-behavior |
Controls whether discrete properties may transition. allow-discrete is newer and older browsers may not support it. |
allow-discrete |
A simple declaration such as transition: opacity 200ms ease; leaves the delay at zero and uses the normal transition behavior. You can set the longhands separately when that is easier to read or override:
.panel {
transition-property: opacity, transform;
transition-duration: 200ms, 300ms;
transition-timing-function: ease, cubic-bezier(.2, .8, .2, 1);
transition-delay: 0ms, 40ms;
}
Comma-separated transition entries pair by position. If the property list has fewer entries than the timing lists, duration, easing, and delay lists repeat or are truncated to match as defined by CSS list matching rules. Keep corresponding lists deliberately aligned to avoid surprising motion. A shorthand entry may use an animatable longhand component even when the changed declaration uses a shorthand such as background.
Choosing properties and using all
transition: all 200ms ease; applies the transition timing to changing properties broadly. It can be convenient while experimenting, but it does not communicate which changes are intended to move. Naming the properties makes that intent easier to inspect and maintain. Only changes to properties covered by the transition will animate.
Choosing duration, easing, and delay
Duration is the time from the start to the destination. Easing describes how the intermediate values progress: linear progresses evenly; ease and custom easing functions vary that progression. Delay postpones the start and is often unnecessary for a simple response. There is no universally correct duration or curve: judge them against the element, the task, and the surrounding interface.
3. Transitioning a class change with JavaScript
JavaScript can supply the state change by adding or removing a class. The transition still belongs in CSS:
<button id="toggle" type="button" aria-expanded="false">Show details</button>
<section id="details" class="details" hidden>More information</section>
<script>
const button = document.querySelector('#toggle');
const details = document.querySelector('#details');
button.addEventListener('click', () => {
const opening = button.getAttribute('aria-expanded') !== 'true';
button.setAttribute('aria-expanded', String(opening));
button.textContent = opening ? 'Hide details' : 'Show details';
if (opening) {
details.hidden = false;
requestAnimationFrame(() => details.classList.add('is-open'));
} else {
details.classList.remove('is-open');
details.addEventListener('transitionend', () => {
if (!details.classList.contains('is-open')) details.hidden = true;
}, { once: true });
}
});
</script>
.details {
opacity: 0;
transform: translateY(-0.25rem);
transition: opacity 180ms ease, transform 180ms ease;
}
.details.is-open {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.details { transition: none; }
}
This illustrates the state pattern, but hiding content requires care: the example waits for a transition event before setting hidden on close. If transitions are disabled or interrupted, production code should also handle that path (for example, by checking reduced motion or using a bounded fallback) so the content does not remain visible or inaccessible. Keep the control’s expanded state synchronized with the content.
4. Entry, exit, and discrete properties
Ordinary transitions need an originating value. For a newly inserted element, there may be no prior rendered style from which to interpolate. The newer @starting-style rule can provide an initial style for such entry cases. Discrete properties, whose values do not interpolate in the usual continuous way, can use transition-behavior: allow-discrete where supported.
.notice {
opacity: 1;
transition: opacity 200ms ease, display 200ms allow-discrete;
}
@starting-style {
.notice {
opacity: 0;
}
}
This is a pattern to adapt to the element’s lifecycle and desired exit behavior, not a universal replacement for a state-management strategy. transition-behavior is newer than the core transition controls; check current compatibility tables for the browsers your users need. The core shorthand, property, duration, timing, and delay features are widely available across browsers, generally established since September 2015 according to MDN.
5. Respecting reduced motion
Motion can clarify state and relationships, but it can also be uncomfortable or harmful for some people, including people with vestibular disorders, epilepsy, migraine, or cognitive concerns. Honor a request for less motion with the prefers-reduced-motion media query. This is one practical measure, not a complete accessibility review for every animated interaction.
@media (prefers-reduced-motion: reduce) {
.animated-element {
transition: none;
}
}
For prominent or persistent motion, consider whether the interface also needs a way to pause or disable it. Preserve the state change and clear feedback even when the animation is removed.
6. Troubleshooting transitions
| Symptom | Likely cause | Fix |
|---|---|---|
| The change is instant | The duration is omitted, so it defaults to 0s, or the duration resolves to zero. |
Set a nonzero transition-duration or include one in the shorthand. |
| Nothing moves | The property value did not change, or the changed property is not in transition-property. |
Inspect computed styles in both states; add the changing property to the transition list. |
| The destination style does not appear | A more specific rule, later declaration, or other state rule overrides it. | Inspect the computed value and cascade for the active state; correct the conflicting selector or declaration. |
| One property has unexpected timing | Comma-separated property, duration, easing, or delay lists do not pair as intended. | Align list entries by position and verify the computed transition values. |
| An element inserted into the page does not fade in | There was no originating style to interpolate from. | Use an explicit starting state where possible, or consider @starting-style in browsers that support it. |
| A discrete change does not animate | The property does not interpolate continuously, or the browser lacks support for discrete transitions. | Consider transition-behavior: allow-discrete and check compatibility; provide a usable non-animated fallback. |
| The transition works on hover but not keyboard focus | The destination state only uses :hover. |
Provide an appropriate :focus-visible state and ensure it remains visibly distinguishable. |
7. Performance, reliability, and browser support
Transitions are a presentation mechanism, not a timing guarantee for application logic. If code needs to run after a transition, the transitionend event can help, but account for transitions being disabled, canceled, or not firing for a particular property. Avoid making correctness depend solely on a visual animation completing.
Keep the property list intentional and test the result in the actual layout and interaction. The research sources establish the transition controls and broad availability of core syntax, but do not establish a universal performance ranking for properties or a best duration. Newer entry and discrete-transition features may vary in older browsers, so consult up-to-date compatibility data for a specific browser or version.
8. Or skip the browser setup
If you need a screenshot of the finished page to document or review the transition, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. See the API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers identify the page verdict and whether it was billed. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
9. FAQ
Does a transition change the element’s state?
No. A pseudo-class, class change, or other CSS state change supplies the new value; the transition interpolates the property change.
Why does my transition play when I hover in but not when I hover out?
Check whether the transition is declared only in the hover rule. Put it on the base rule so it applies in both directions.
Can every CSS property transition?
No. A property needs transitionable values for the behavior you want. Discrete transitions are a newer, separate capability, and support can vary.
Are the core transition properties supported across browsers?
They are widely available, but verify current browser compatibility for a specific feature and target audience, especially for transition-behavior and @starting-style.


