You’ve already seen what happens when you hover over a button and its color flips instantly — one frame it’s blue, the next it’s darker, no in-between. That hard snap works, but it feels cheap. Real, polished interfaces ease from one state to another: a button gently darkens, a card lifts smoothly, a menu slides open. That smoothness is what separates a page that works from a page that feels good.
The wonderful part is you don’t need JavaScript for any of it. CSS transitions let you say “when this property changes, don’t jump straight to the new value — glide there over a fraction of a second.” One line of CSS, and every instant change becomes a smooth animation. Let’s learn exactly how.
What a transition actually does
A transition watches a property for changes and, instead of letting it snap to the new value, animates it over a duration you choose. The trigger is anything that changes the property: a :hover, a class toggled by JavaScript, a state change. The transition itself only describes how the change should play out.
Here’s the simplest possible example — a button whose background fades on hover:
.button {
background-color: #00b8e6;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0090b8;
}
Without the transition line, hovering would flip the color instantly. With it, the color slides from the lighter blue to the darker one over 0.3 seconds — and slides back just as smoothly when your cursor leaves. Notice that the transition lives on the base state (.button), not on :hover. That’s deliberate, and it matters.
Put the transition on the base state, not the hover
A common beginner mistake is writing the transition inside .button:hover. Do that and the animation only plays on the way in — leaving the element snaps back instantly. Declare the transition on the resting state (.button) and it applies in both directions: mouse in and mouse out. The rule of thumb: transitions describe the element’s normal behavior, so they belong on its normal style.
The four transition properties
The transition shorthand bundles up to four separate properties. Understanding each on its own makes the shorthand obvious later.
| Property | What it controls | Example |
|---|---|---|
transition-property |
Which property to animate | background-color |
transition-duration |
How long it takes | 0.3s |
transition-timing-function |
The speed curve | ease-in-out |
transition-delay |
How long to wait before starting | 0.1s |
Written out the long way, our button looks like this:
.button {
transition-property: background-color;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0s;
}
You’ll rarely write all four out — the shorthand is far tidier — but it helps to know that’s what’s happening under the hood. Let’s look at each one.
transition-property — what to animate
This names the property (or properties) that should animate. You can target one property, several, or use the keyword all to animate every property that changes:
/* just one property */
transition-property: opacity;
/* a few specific ones, comma-separated */
transition-property: opacity, transform;
/* everything that changes */
transition-property: all;
all is convenient, but be a little careful with it. It tells the browser to watch every animatable property, which can cause surprise animations on things you didn’t mean to touch. Naming the exact properties you care about is clearer and usually performs better.
transition-duration — how long it takes
The duration is how long the animation runs, in seconds (s) or milliseconds (ms):
transition-duration: 0.3s; /* same as 300ms */
Duration is the single biggest lever on how a transition feels. Too fast and it’s barely there; too slow and the interface feels sluggish. For hover effects and small UI changes, somewhere between 150ms and 300ms hits the sweet spot — quick enough to feel responsive, slow enough to read as smooth.
A transition needs a duration to exist
If you don’t set a duration (or set it to 0s), there’s no transition at all — the change happens instantly, exactly as if you’d never written the property. The duration is what gives the animation time to play. It’s the one part you can never leave out.
transition-timing-function — the speed curve
A transition doesn’t have to move at a constant speed. The timing function shapes the acceleration — whether it starts slow and speeds up, races out and coasts to a stop, or stays even the whole way. This is the detail that makes motion feel natural instead of robotic.
transition-timing-function: ease; /* default: slow start, fast middle, slow end */
transition-timing-function: linear; /* constant speed */
transition-timing-function: ease-in; /* slow start, fast finish */
transition-timing-function: ease-out; /* fast start, slow finish */
transition-timing-function: ease-in-out; /* slow at both ends */
The differences are subtle but real. ease-out is a great default for things appearing or moving into view — it feels snappy and confident, decelerating gently as it arrives. linear looks mechanical and is best saved for things like spinners that genuinely should move at a steady pace. If you want full control, cubic-bezier() lets you draw your own curve, but the built-in keywords cover the vast majority of real work.
transition-delay — wait before starting
A delay holds the transition back before it begins:
transition-delay: 0.1s;
Most of the time you’ll leave this at 0s. Where it earns its keep is in staggered effects — making several elements animate one after another by giving each a slightly larger delay — or in tooltips that should wait a beat before appearing so they don’t flash on every accidental hover.
The shorthand
Once the four pieces make sense, the transition shorthand collapses them into one tidy line. The order is property, duration, timing-function, delay:
.button {
transition: background-color 0.3s ease-in-out 0s;
}
In practice most people drop the parts they don’t need. If there’s no delay, leave it off. The two values you almost always supply are the property and the duration:
.button {
transition: background-color 0.3s;
}
When the two time values appear together, the first one is always the duration and the second is the delay — that’s how the browser tells them apart. So transition: opacity 0.3s 0.1s means a 0.3s animation that waits 0.1s before starting.
Animating multiple properties
Real effects often change more than one thing at once — say, a card that both lifts and brightens. You list each property’s transition separated by commas, and you can give each its own timing:
.card {
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
Here the card rises six pixels and its shadow grows, both over the same 0.3 seconds, producing that satisfying “lifting off the page” feel. You could shorten this to transition: all 0.3s ease-out, and it would work — but naming transform and box-shadow explicitly keeps you in control of exactly what animates.
Prefer transform and opacity for smooth animation
Browsers can animate transform and opacity extremely efficiently because they don’t force the page to recalculate layout. Animating properties like width, height, top, or margin makes the browser re-measure and repaint the page on every frame, which can stutter on slower devices. Whenever you can express a movement as a transform (e.g. translateY instead of changing top), do it — your animations will be noticeably smoother.
What you can and can’t transition
Not every property can be animated. The general rule: a property is animatable if the browser can compute the in-between values. Colors, lengths, opacity, and transforms all have a clear “halfway point,” so they transition beautifully. Things that don’t have intermediate states can’t.
The classic gotcha is display. You cannot smoothly transition display: none to display: block — there’s no halfway between “not in the layout” and “in the layout,” so the change is always instant. This trips up everyone at some point: people try to fade in a hidden menu by transitioning display and wonder why nothing animates.
The fix is to animate opacity (and often visibility) instead:
.menu {
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s;
}
.menu.open {
opacity: 1;
visibility: visible;
}
The menu fades in and out via opacity, while visibility flips it between clickable and not. This is the standard pattern for showing and hiding things smoothly, and it’s worth committing to memory.
A practical example: a polished button
Let’s bring the core ideas together into a button you’d genuinely ship — color, lift, and shadow all easing together on hover:
.btn {
background-color: #00b8e6;
color: #ffffff;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
.btn:hover {
background-color: #0090b8;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
<button class="btn">Get Started</button>
On hover the button darkens slightly, nudges up two pixels, and grows a soft shadow — all over a brisk 0.2 seconds. On :active (while it’s being clicked) it presses back down, giving that tactile “pushed” feeling. None of this needs a single line of JavaScript, and it’s the kind of detail people feel even if they never consciously notice it.
A note on accessibility
Motion is delightful for most people, but for some it causes genuine discomfort or even nausea. Browsers expose a setting for this, and you should respect it. The prefers-reduced-motion media query lets you switch off or tone down animations for users who’ve asked their system to reduce motion:
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
}
}
This effectively disables transitions for anyone who’s opted out, without you having to rewrite every rule. It’s a small addition that makes your site considerably more considerate, and it’s a hallmark of professional front-end work. If you want a refresher on media queries, our guide to responsive design covers how they work.
Wrapping up
CSS transitions are the easiest way to make an interface feel alive, and you’ve now got the whole toolkit:
- A transition animates a property smoothly when it changes — triggered by
:hover, a class toggle, or any state change. - Declare it on the base state, not the hover, so it plays in both directions.
- The four pieces are
transition-property,transition-duration,transition-timing-function, andtransition-delay— bundled by the shorthand in that order. - Duration controls the feel (150–300ms for most UI); the timing function shapes the speed curve (
ease-outis a great default). - Animate multiple properties with comma-separated values, and favor
transformandopacityfor smooth performance. - You can’t transition
display— fade withopacityandvisibilityinstead. - Respect
prefers-reduced-motionso motion-sensitive users have a comfortable experience.
Transitions handle the simple case beautifully: a change from one state to another. But when you need motion that runs on its own — looping, multi-step, playing without any user interaction — there’s a more powerful tool waiting. Before that, though, you’ll want to master the property that makes movement smooth in the first place: CSS transforms, which is up next.