CSS transitions are the simplest way to add motion to a webpage โ they smoothly animate a property from one value to another whenever that property changes. A single transition declaration turns an instant state switch into a fluid, polished interaction. In this lesson you will learn all four transition properties, master timing functions, and understand exactly which CSS properties are animatable.
The Four Transition Properties
| Property | Controls | Example Value |
|---|---|---|
transition-property |
Which CSS property to animate | background-color, transform, all |
transition-duration |
How long the animation takes | 0.3s, 200ms |
transition-timing-function |
Acceleration curve โ how the value changes over time | ease, linear, cubic-bezier() |
transition-delay |
Wait before the animation starts | 0s, 0.1s |
transition |
Shorthand: property duration timing-function delay | background-color 0.3s ease 0s |
Built-in Timing Functions
| Keyword | cubic-bezier Equivalent | Feel |
|---|---|---|
ease |
cubic-bezier(0.25, 0.1, 0.25, 1) |
Default โ fast start, gentle deceleration |
linear |
cubic-bezier(0, 0, 1, 1) |
Constant speed โ mechanical, robotic feel |
ease-in |
cubic-bezier(0.42, 0, 1, 1) |
Slow start, fast end โ feels like launching |
ease-out |
cubic-bezier(0, 0, 0.58, 1) |
Fast start, gentle landing โ natural and common |
ease-in-out |
cubic-bezier(0.42, 0, 0.58, 1) |
Slow start and end โ symmetric, refined |
cubic-bezier(x1,y1,x2,y2) |
Custom curve | Full control โ use tools like easings.net |
Animatable vs Non-Animatable Properties
| Animatable โ | Not Animatable โ | Notes |
|---|---|---|
opacity, transform |
display |
display cannot transition between none and block |
color, background-color |
font-family |
font-family changes are instant |
width, height, padding, margin |
float, position |
Positional layout modes cannot interpolate |
border-radius, box-shadow |
content |
Generated content is non-animatable |
font-size, line-height |
background-image (in most cases) |
Gradient to gradient can transition in modern browsers |
transform and opacity over width, height, top, or left. Transforms and opacity are composited on the GPU and do not trigger layout recalculation โ they run at 60fps even on complex pages. Transitioning layout properties like width forces the browser to recompute the entire layout on every frame, causing jank.transition: transform 0.3s ease, opacity 0.2s ease, box-shadow 0.3s ease. Each property gets its own duration and easing โ far more precise than transition: all 0.3s, which applies the same timing to every property change including unexpected ones.transition: all in production code. It transitions every property that changes โ including width, height, and color during JavaScript-driven class changes โ often producing unintended animations. Always specify exactly which properties should transition.Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<title>CSS Transitions</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 100%; }
body { font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; padding: 48px 32px; }
h3 { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 40px 0 16px; }
.row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin-bottom: 8px; }
/* โโ Button transitions โโ */
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
/* Colour transition */
.btn-color {
background: #4f46e5;
color: white;
transition: background-color 0.25s ease;
}
.btn-color:hover { background: #4338ca; }
/* Transform + shadow transition */
.btn-lift {
background: white;
color: #0f172a;
border: 1.5px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-lift:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
/* Multiple properties */
.btn-multi {
background: #0f172a;
color: white;
transition: background-color 0.3s ease,
transform 0.2s ease,
letter-spacing 0.3s ease;
}
.btn-multi:hover {
background: #4f46e5;
transform: scale(1.04);
letter-spacing: 0.05em;
}
/* Timing function comparison */
.timing-demo {
position: relative;
height: 60px;
background: #f1f5f9;
border-radius: 10px;
overflow: hidden;
margin-bottom: 8px;
max-width: 600px;
}
.timing-ball {
position: absolute;
top: 50%;
left: 8px;
transform: translateY(-50%);
width: 44px; height: 44px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.65rem; font-weight: 700; color: white;
transition-property: left;
transition-duration: 1.2s;
}
.timing-demo:hover .timing-ball { left: calc(100% - 52px); }
.ball-ease { background: #4f46e5; transition-timing-function: ease; }
.ball-linear { background: #0891b2; transition-timing-function: linear; }
.ball-ease-in { background: #7c3aed; transition-timing-function: ease-in; }
.ball-ease-out { background: #10b981; transition-timing-function: ease-out; }
.ball-ease-in-out { background: #f59e0b; transition-timing-function: ease-in-out; }
/* Card hover โ multiple property transition */
.card-demo {
width: 220px;
background: white;
border-radius: 14px;
border: 1.5px solid #e2e8f0;
padding: 24px;
cursor: pointer;
transition: transform 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
}
.card-demo:hover {
transform: translateY(-6px);
box-shadow: 0 20px 40px rgba(79,70,229,0.12);
border-color: #a5b4fc;
}
.card-demo-title { font-size: 0.95rem; font-weight: 700; color: #0f172a; margin-bottom: 6px; }
.card-demo-text { font-size: 0.8rem; color: #64748b; line-height: 1.5; }
/* Transition with delay โ staggered */
.stagger-row { display: flex; gap: 10px; }
.stagger-item {
width: 48px; height: 48px;
background: #4f46e5;
border-radius: 10px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.stagger-item:nth-child(1) { transition-delay: 0s; }
.stagger-item:nth-child(2) { transition-delay: 0.06s; }
.stagger-item:nth-child(3) { transition-delay: 0.12s; }
.stagger-item:nth-child(4) { transition-delay: 0.18s; }
.stagger-item:nth-child(5) { transition-delay: 0.24s; }
.stagger-row:hover .stagger-item {
transform: translateY(-12px);
opacity: 0.7;
}
</style>
</head>
<body>
<h3>Button Transitions โ hover each</h3>
<div class="row">
<button class="btn btn-color">Colour fade</button>
<button class="btn btn-lift">Lift + shadow</button>
<button class="btn btn-multi">Multi-property</button>
</div>
<h3>Timing Functions โ hover the track to race the balls</h3>
<div style="display:grid;gap:8px;max-width:600px">
<div class="timing-demo"><div class="timing-ball ball-ease">ease</div></div>
<div class="timing-demo"><div class="timing-ball ball-linear">linear</div></div>
<div class="timing-demo"><div class="timing-ball ball-ease-in">in</div></div>
<div class="timing-demo"><div class="timing-ball ball-ease-out">out</div></div>
<div class="timing-demo"><div class="timing-ball ball-ease-in-out">in-out</div></div>
</div>
<h3>Card Hover โ transform + shadow + border</h3>
<div class="row">
<div class="card-demo">
<p class="card-demo-title">Hover me</p>
<p class="card-demo-text">Three properties transition independently with the same duration and easing.</p>
</div>
</div>
<h3>Staggered Delay โ hover the row</h3>
<div class="stagger-row">
<div class="stagger-item"></div>
<div class="stagger-item"></div>
<div class="stagger-item"></div>
<div class="stagger-item"></div>
<div class="stagger-item"></div>
</div>
</body>
</html>
How It Works
Step 1 โ Transition Lives on the Base State
The transition property is always declared on the element’s default state, not on :hover. This means the transition plays both ways โ when entering the hover state and when leaving it. If you put transition only on :hover, the animation plays when hovering but snaps back instantly when you move the cursor away.
Step 2 โ GPU-Composited Properties Stay at 60fps
transform: translateY(-3px) moves the element visually without affecting the document layout. The browser hands this to the GPU compositor, which can animate it every frame without recalculating widths, heights, or positions of other elements. The result is butter-smooth motion even on content-heavy pages.
Step 3 โ Multiple Transitions with Comma Separation
The .btn-multi button transitions background-color at 0.3s, transform at 0.2s, and letter-spacing at 0.3s โ each with its own duration. The background and letter-spacing complete together while the scale completes slightly earlier, creating a layered, non-mechanical feel.
Step 4 โ transition-delay Creates Stagger
Each .stagger-item has the same transition but a different transition-delay โ 0s, 0.06s, 0.12s, 0.18s, 0.24s. When the parent is hovered, the items animate in sequence with a 60ms gap between each, creating a wave effect from a few lines of CSS and no JavaScript.
Step 5 โ ease-out for Entering, ease-in for Exiting
Natural motion decelerates as it arrives โ use ease-out for elements that appear or arrive (modals, dropdowns, tooltips). For elements that leave, ease-in (slow start, fast exit) feels more natural. For hover states that play in both directions, ease is a good compromise.
Real-World Example: Navigation with Animated Underline Indicator
/* animated-nav.css */
.nav { display: flex; gap: 4px; padding: 0 8px; }
.nav-item {
position: relative;
padding: 10px 16px;
font-size: 0.875rem;
font-weight: 500;
color: #64748b;
text-decoration: none;
border-radius: 8px;
transition: color 0.2s ease, background-color 0.2s ease;
}
/* Animated underline via pseudo-element */
.nav-item::after {
content: '';
position: absolute;
bottom: 4px;
left: 16px;
right: 16px;
height: 2px;
background: #4f46e5;
border-radius: 2px;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.25s ease-out;
}
.nav-item:hover { color: #0f172a; background: #f8fafc; }
.nav-item:hover::after { transform: scaleX(1); }
.nav-item.active { color: #4f46e5; font-weight: 600; }
.nav-item.active::after { transform: scaleX(1); }
/* โโ Animated toggle switch โโ */
.toggle {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.toggle-track {
width: 44px; height: 24px;
background: #cbd5e1;
border-radius: 9999px;
position: relative;
transition: background-color 0.25s ease;
}
.toggle-thumb {
position: absolute;
top: 3px; left: 3px;
width: 18px; height: 18px;
background: white;
border-radius: 50%;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
input[type="checkbox"]:checked + .toggle-track { background: #4f46e5; }
input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
transform: translateX(20px);
}
input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
/* โโ Animated dropdown reveal โโ */
.dropdown-menu {
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
opacity: 0;
transform: translateY(-8px) scale(0.97);
pointer-events: none;
transition: opacity 0.2s ease-out,
transform 0.2s ease-out;
}
.dropdown-trigger:focus-within .dropdown-menu,
.dropdown-trigger:hover .dropdown-menu {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
Common Mistakes
Mistake 1 โ Transitioning layout properties
โ Wrong โ width/height trigger expensive layout recalculations on every frame:
.box { width: 100px; transition: width 0.3s ease; }
.box:hover { width: 200px; } /* forces full layout reflow each frame */
โ Correct โ use transform: scaleX() for visual resize without layout cost:
.box { transform: scaleX(1); transform-origin: left; transition: transform 0.3s ease; }
.box:hover { transform: scaleX(2); } /* GPU-composited โ no layout thrash */
Mistake 2 โ Putting transition only on :hover
โ Wrong โ the exit animation snaps instantly when hover ends:
.btn:hover { background: #4f46e5; transition: background 0.3s ease; }
/* On mouse-out: background snaps back with no transition */
โ Correct โ transition on the base element applies in both directions:
.btn { background: #6366f1; transition: background-color 0.3s ease; }
.btn:hover { background: #4f46e5; }
Mistake 3 โ Using transition: all in production
โ Wrong โ all catches every property change, including ones you did not intend to animate:
.card { transition: all 0.3s ease; }
/* If JS adds a class that changes width, height, colour โ all animate unexpectedly */
โ Correct โ enumerate exactly the properties that should animate:
.card { transition: transform 0.25s ease, box-shadow 0.25s ease; }
Quick Reference
| Property | Example | Notes |
|---|---|---|
transition |
transform 0.3s ease |
Shorthand: property duration timing delay |
transition-property |
transform, opacity |
Prefer transform + opacity for performance |
transition-duration |
0.25s |
100โ400ms for UI; longer for emphasis |
transition-timing-function |
ease-out |
ease-out for arrivals; ease-in for departures |
transition-delay |
0.1s |
Stagger by incrementing delay on siblings |
| Multiple transitions | transform 0.3s ease, opacity 0.2s ease |
Comma-separated โ each property independent |
| GPU properties | transform, opacity |
Always prefer over width/height/top/left |