From one state to another
Normally, when a property changes, say a button's colour on hover, the new value appears instantly. A transition tells the browser to animate the change over a short time. It is the simplest kind of CSS animation: you describe a start and an end state, and the browser fills in the frames.
<style>
.btn {
padding: 10px 22px;
border: 0;
border-radius: 8px;
background: #4f46e5;
color: white;
font-size: 16px;
cursor: pointer;
transition: background-color 0.25s ease, transform 0.25s ease;
}
.btn:hover { background: #7c3aed; transform: translateY(-3px); }
.btn:active { transform: translateY(0); }
</style>
<button class="btn">Hover over me</button>Move the pointer on and off. The colour and position change smoothly in both directions, because the transition is declared on .btn (the base state), not only on :hover.
The transition shorthand
transition: property duration timing-function delay;- property: what to animate:
background-color,transform,opacity, orall. Name properties explicitly;allcan animate things you did not intend. - duration: how long, in seconds (
0.25s) or milliseconds (250ms). Required. - timing-function: the pacing curve.
- delay: wait before starting.
You can list several transitions separated by commas, as above.
How long?
The right duration depends on the size of the change:
- 100–200 ms for small feedback: hover colour, button press.
- 200–400 ms for larger movement: menus, cards, panels.
- Over 500 ms feels slow for interface responses.
Quick and quiet is better than showy. The aim is to make a change understandable, not to be noticed.
Easing: the feel of motion
The timing function controls speed along the way:
ease(default): quick start, gentle end.ease-out: starts fast, slows down. Good for things entering, as they arrive naturally.ease-in: starts slow, speeds up. Good for things leaving.ease-in-out: slow at both ends.linear: constant speed. Right for spinners and progress, wrong for most UI movement.cubic-bezier(x1, y1, x2, y2): a custom curve;steps(n)jumps in stages.
.menu { transition: opacity 0.2s ease-out, transform 0.2s ease-out; }Transition the base state
A very common mistake is putting the transition only in the hover rule:
/* Animates on the way in, snaps back when you move away */
.card:hover { transition: transform 0.3s; transform: scale(1.03); }
/* Animates both ways */
.card { transition: transform 0.3s; }
.card:hover { transform: scale(1.03); }Put it on the element itself and every change to the listed properties animates, whichever way it goes.
What can and cannot be animated
A property can be transitioned only if the browser can calculate in-between values: colours, lengths, numbers, transforms. Things with no middle ground, such as display: none to block, position, or font-family, switch instantly.
To fade something in and out, animate opacity (and combine with visibility, or the hidden attribute when finished). Modern CSS has transition-behavior: allow-discrete and @starting-style to animate display, but support is still arriving, so check before you rely on them.
Keep it fast
Animating some properties forces the browser to re-calculate layout or repaint on every frame:
- Cheap:
transform(move, scale, rotate) andopacity. - Costly:
width,height,top,left,margin,padding,box-shadowon big areas.
Where you can, express movement with transform: translate() rather than left/top, and size changes with transform: scale(). Your page will stay smooth on cheap phones.
Accessible motion
Not everyone enjoys movement. For some, large or constant motion causes dizziness. Provide a calm version for those who ask:
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; }
}Also:
- Do not rely on a transition to communicate something essential; the end state must be clear on its own.
- Keep
:focus-visiblestyles immediate and obvious.
Example: a card that lifts
<style>
.card {
max-width: 220px; padding: 20px; border-radius: 12px; background: white;
box-shadow: 0 2px 6px rgb(0 0 0 / .12);
transition: transform .2s ease-out, box-shadow .2s ease-out;
}
.card:hover, .card:focus-within { transform: translateY(-4px); box-shadow: 0 10px 24px rgb(0 0 0 / .18); }
body { background: #f1f5f9; padding: 20px; }
@media (prefers-reduced-motion: reduce) { .card { transition: none; } }
</style>
<div class="card"><h3 style="margin-top:0">Lift</h3><a href="#">Read more</a></div>:focus-within makes the card lift for keyboard users too.
Common mistakes
- Transition only on
:hover, so it snaps back. transition: allanimating unexpected properties.- Very slow transitions that make the interface feel sluggish.
- Animating layout properties on large elements.
- Hover-only effects with no keyboard or touch equivalent.
Exercise
Create a link-style button whose underline grows from left to right on hover (use a pseudo-element or background-size), with a 0.25s transition, and no motion for people who prefer reduced motion.
Show one possible answer
<style>
.link {
text-decoration: none; color: #1d4ed8; font-weight: 600;
background: linear-gradient(currentColor, currentColor) no-repeat left bottom / 0 2px;
transition: background-size .25s ease-out;
}
.link:hover, .link:focus-visible { background-size: 100% 2px; }
@media (prefers-reduced-motion: reduce) { .link { transition: none; } }
</style>
<a class="link" href="#">Read the guide</a>Key points
- A transition animates a property from its old value to its new value instead of switching instantly.
- Put the transition on the base state so it works in both directions, not only on :hover.
- transition takes property, duration, timing function and delay; durations of 150 to 300 ms suit most interface changes.
- Animate transform and opacity where possible, since they are cheap for the browser.
- Respect prefers-reduced-motion and never make important feedback depend on motion alone.