CSS Transitions: Smooth State Changes
CSS transitions allow property values to change smoothly over a specified duration, instead of switching abruptly. They are the foundation of polished, professional web interactions.
1. The Four Core Properties
| Property | Description | Example |
|---|---|---|
transition-property | Which CSS property to animate | background-color, transform, all |
transition-duration | How long the transition takes | 0.3s, 300ms |
transition-timing-function | The speed curve | ease, linear, cubic-bezier(...) |
transition-delay | Wait before starting | 0s, 0.1s |
/* Longhand */
.button {
transition-property: background-color, transform, box-shadow;
transition-duration: 0.25s, 0.2s, 0.3s;
transition-timing-function: ease, ease-out, ease;
transition-delay: 0s, 0s, 0s;
}
/* Shorthand (recommended) */
.button {
transition: background-color 0.25s ease,
transform 0.2s ease-out,
box-shadow 0.3s ease;
}
2. The all Shortcut
transition: all 0.3s ease; animates every changing property simultaneously. Use it for prototyping but be more specific in production — animating all can accidentally animate properties like height or width which causes expensive layout recalculations.
3. Timing Functions in Depth
The timing function controls the rate of change throughout the animation duration.
| Function | Behavior | Best For |
|---|---|---|
ease | Slow → Fast → Slow | Default, most natural feeling |
ease-in | Slow start → Fast end | Elements leaving the screen |
ease-out | Fast start → Slow end | Elements entering the screen |
ease-in-out | Slow → Fast → Slow (symmetrical) | Toggles, modals opening/closing |
linear | Constant speed | Loading bars, rotating loaders |
cubic-bezier(x1,y1,x2,y2) | Custom curve | Bouncy or spring-like effects |
steps(n) | Jumps in N steps | Sprite animations, typewriter effect |
Custom Cubic Bezier
Use cubic-bezier.com to visually design curves. Copy the output:
/* A satisfying "spring" feel */
.btn { transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
4. Triggering Transitions
Transitions fire when a CSS property changes. Common triggers:
/* :hover — most common */
.card:hover { transform: translateY(-4px); }
/* :focus — keyboard / form accessibility */
.input:focus { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(108,99,255,0.3); }
/* :active — button press feedback */
.btn:active { transform: scale(0.97); }
/* Class toggled by JavaScript */
.modal { opacity: 0; pointer-events: none; }
.modal.is-open { opacity: 1; pointer-events: auto; }
5. The transition-delay Use Case
Stagger animations on a group of elements for a polished reveal effect:
.nav__item:nth-child(1) { transition-delay: 0ms; }
.nav__item:nth-child(2) { transition-delay: 50ms; }
.nav__item:nth-child(3) { transition-delay: 100ms; }
.nav__item:nth-child(4) { transition-delay: 150ms; }
6. Performance: What to Animate
Only certain properties can be transitioned with GPU acceleration (cheap). Others trigger layout recalculation (expensive).
| ✅ GPU-Accelerated (Use These) | ❌ Triggers Layout (Avoid) |
|---|---|
transform | width, height |
opacity | top, left, bottom, right |
filter | margin, padding |
clip-path | font-size |
/* ❌ Bad — triggers layout every frame */
.elem:hover { width: 300px; left: 100px; }
/* ✅ Good — GPU-composited, 60fps smooth */
.elem:hover { transform: scaleX(1.5) translateX(100px); }
7. Respecting Reduced Motion
Always honor users who prefer less motion (vestibular disorders, epilepsy):
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
8. Practical Polished Button Example
.btn {
background: var(--color-primary, #6c63ff);
color: white;
padding: 0.8rem 1.75rem;
border-radius: 8px;
border: none;
cursor: pointer;
/* Transition specific properties, not all */
transition:
background-color 0.2s ease,
transform 0.15s ease-out,
box-shadow 0.2s ease;
}
.btn:hover {
background: var(--color-primary-dark, #5a52d5);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(108, 99, 255, 0.4);
}
.btn:active {
transform: translateY(0);
box-shadow: none;
}