Learn CSS transitions and animations from scratch — hover effects, loading spinners, scroll-triggered animations, and performance best practices.
CSS animations are one of the most powerful tools in a frontend developer's kit. Done right, they make UIs feel alive and responsive. Done wrong, they tank performance and annoy users. This guide covers everything from simple hover transitions to complex multi-step keyframe sequences.
CSS transitions animate a property from one value to another when it changes:
.button {
background-color: #2563eb;
transform: scale(1);
/* transition: property duration timing-function delay */
transition: background-color 200ms ease, transform 150ms ease;
}
.button:hover {
background-color: #1d4ed8;
transform: scale(1.02);
}
| Function | Behavior |
|---|---|
ease | Slow start, fast middle, slow end (default) |
linear | Constant speed |
ease-in | Slow start, fast end |
ease-out | Fast start, slow end |
ease-in-out | Slow start and end |
cubic-bezier(x1,y1,x2,y2) | Custom curve |
steps(4) | 4 discrete steps (good for sprite animation) |
For entrance animations, use ease-out (starts fast, settles). For exit animations, use ease-in (accelerates out of view).
Use @keyframes for multi-step animations that run automatically:
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn 300ms ease-out forwards;
/* name dur timing fill-mode */
}
.element {
animation:
name /* @keyframes name */
duration /* e.g., 500ms */
timing-function /* e.g., ease-out */
delay /* e.g., 100ms */
iteration-count /* e.g., infinite, 3 */
direction /* e.g., alternate */
fill-mode /* forwards, backwards, both */
play-state; /* running, paused */
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.skeleton {
background: #e5e7eb;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notification {
animation: slideUp 250ms ease-out;
}
.list-item {
opacity: 0;
animation: fadeIn 300ms ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
.list-item:nth-child(4) { animation-delay: 150ms; }
Or generate delays dynamically with JavaScript/React:
{items.map((item, i) => (
<li key={item.id} style={{ animationDelay: `${i * 50}ms` }}>
{item.name}
</li>
))}
The GPU can accelerate only certain CSS properties. Animating others forces layout recalculation (reflow) — which is expensive.
| Property | Cost | Notes |
|---|---|---|
transform | Free (GPU) | Translate, scale, rotate |
opacity | Free (GPU) | Perfect for fade effects |
filter | Moderate (GPU sometimes) | Blur, brightness |
color, background-color | Cheap (repaint) | No reflow |
width, height, margin, padding | Expensive (reflow) | Causes layout |
top, left, right, bottom | Expensive (reflow) | Use transform instead |
Golden rule: Animate transform and opacity. Everything else has a cost.
/* ❌ Expensive — causes reflow */
.bad { transition: height 300ms; }
/* ✅ GPU-accelerated */
.good { transition: transform 300ms; }
Always respect prefers-reduced-motion. Some users experience motion sickness from animations:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Or check in JavaScript:
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
Use HeoLab's CSS Animation Generator to configure timing, easing, and keyframes visually — then copy the generated CSS straight into your project.