What does a transition do?
Without a transition, a change in a CSS value happens in a single frame. Hover a button and its colour flips. A transition tells the browser to animate between the old and new value over a set time, so the colour fades instead.
Transitions react to state changes you already have: :hover, :focus-visible, a class added by JavaScript, or a media query that switches on. You do not describe keyframes. You describe which properties should animate, for how long, and with what pace, and the browser fills in the frames between the two states.


linear moves at a constant speed. ease-out starts fast and settles gently, which makes interface feedback feel quick and calm.Which four properties control a transition?
transition-property names what should animate, such as background-color or transform. transition-duration sets how long it takes, in seconds or milliseconds; computed styles always report it in seconds, so 200ms shows as 0.2s. transition-timing-function sets the pace within that time. transition-delay sets how long to wait before starting.
The default duration is 0s, which is why nothing animates until you set one. A transition with a property but no duration does nothing at all.
You can list several properties with matching comma-separated durations, so a card can fade its shadow over 0.3s and move over 0.2s.
.btn {
transition-property: background-color;
transition-duration: 0.2s;
transition-timing-function: ease-out;
transition-delay: 0s;
}How do you write the shorthand?
The transition shorthand takes the property, the duration, the timing function and the delay, in that order, and you can leave off the ones you do not need. The first time value is always the duration and the second is the delay. Separate several transitions with commas.
If you leave out the property, it defaults to all, which animates every property that changes. That is quick to write, but it will also animate properties you change later for other reasons, including layout properties that are slow to animate. Naming properties keeps the behaviour predictable.
A missing unit breaks the whole line. transition: transform 0.25 ease-out is invalid, so the browser ignores it and the change snaps. AI-generated CSS makes this slip regularly.
.card {
transition:
transform 0.2s ease-out,
box-shadow 0.3s ease-out;
}How do timing functions change the feel?
The timing function maps time to progress. ease, the default, starts slowly, speeds up and slows down at the end. ease-out starts fast and settles gently, which suits things responding to the user, like a button changing colour or a menu opening: the feedback starts immediately. ease-in starts slowly and finishes fast, which suits things leaving the screen. linear moves at a constant speed, which looks mechanical for movement but is fine for colour and opacity.
For custom curves, cubic-bezier(x1, y1, x2, y2) sets the two control points of the curve. Values above 1 for the y numbers make a slight overshoot, as in cubic-bezier(0.34, 1.56, 0.64, 1), for a playful bounce. DevTools shows an easing editor when you click the curve icon next to a timing function, and you can drag the points and watch the result.
For durations, small interface changes feel right between 150ms and 300ms. Larger movements, such as a panel sliding in, can go up to about 400ms. Longer than that and the interface feels slow.
.menu {
transition: opacity 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}Which properties animate smoothly?
Every frame of a transition the browser may need to recalculate layout, repaint pixels, or only recombine layers. Changing width, height, margin or top forces layout on each frame, which can stutter on a slow phone, especially on a large page. Changing color or background-color needs a repaint, which is cheaper.
Changing transform and opacity is cheapest: the browser can usually move or fade an existing layer on the graphics card without touching layout or paint. So to lift a card, transition transform: translateY(-4px) instead of margin-top. To grow a button, use transform: scale(1.05) instead of width.
Some properties cannot be interpolated at all. display and font-family have no in-between values, so by default they switch at once even inside a transition list.
Why put the transition on the base state?
A transition applies whenever the value changes while the transition is in effect. If you put it on the base .btn rule, it applies in both directions: fading in on hover and fading out when the pointer leaves. If you put it only on .btn:hover, the enter animates but the exit snaps, because the moment the hover ends, the hover rule and its transition no longer apply.
Pair :hover with :focus-visible so keyboard users see the same feedback as mouse users. :focus-visible matches when the browser shows a focus ring, typically for keyboard focus, and not after a mouse click. Using plain :focus leaves a card lifted after someone clicks it.
.btn {
transition: background-color 0.2s ease-out;
}
.btn:hover,
.btn:focus-visible {
background-color: #b5442c;
}How do you respect reduced motion?
Some people get dizzy or nauseous from movement on screen, and they turn on a reduced motion setting in their operating system. The prefers-reduced-motion: reduce media query tells you. Inside it, remove transitions that move, scale or slide things, or swap them for a short fade. Colour and opacity changes are generally safe to keep.
A tidy pattern is to keep movement out of the base styles and add it only inside @media (prefers-reduced-motion: no-preference). Then visitors who never set the preference get motion, and nobody gets surprise movement if the query fails.
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}Can you transition display or an element appearing?
Two newer features handle elements that appear and disappear. transition-behavior: allow-discrete lets properties like display and overlay take part in a transition, flipping at the right moment so a fade-out can finish before the element is hidden. @starting-style defines the values an element starts from on its first render, so a dialog or popover can fade in from opacity: 0 as it is shown.
Both are supported in Chrome, Edge and Safari, and in Firefox from 2024 for @starting-style, with display transitions still patchy there. Treat them as an enhancement: without support, the element still appears and disappears, it only skips the fade.
.popover {
transition: opacity 0.2s, display 0.2s allow-discrete;
}
@starting-style {
.popover:popover-open {
opacity: 0;
}
}Field notes
- A transition animates between an old value and a new one instead of changing in one frame.
- The shorthand lists property, duration, timing function and delay, in that order.
ease-outstarts fast and settles gently, which suits most interface changes.- Put the transition on the base rule so it runs both when entering and leaving the hover.
Easy to get wrong
On the hover rule alone, the transition runs on the way in only. The exit snaps back.
Durations need a unit. Without it the whole shorthand is invalid and nothing animates.
Animating margin recalculates layout every frame and shifts neighbours. A transform moves only the card and runs smoothly.
all animates every change, including slow layout properties you did not intend to animate.
Questions people ask
What is the difference between a transition and an animation?
A transition animates between two states when a value changes. An animation runs keyframes you define and can loop or start on its own.
Why does my transition not run when the page loads?
Transitions need a change from a previous value. On first render there is none, unless you use @starting-style.
Can I transition height to auto?
Not with a plain transition in most browsers. Newer interpolate-size: allow-keywords enables it in Chrome only so far.
Do transitions run when JavaScript adds a class?
Yes. Any change in a computed value triggers the transition, whatever caused it.