On this page
CSS transitions and animations
Transitions vs animations
CSS offers two mechanisms for adding motion:
- Transitions: animate the change between two states (for example, hover)
- Animations with @keyframes: define complex sequences with multiple steps
Transitions
A transition smooths the change of a CSS property from one value to another. It is defined on the base state (not on :hover).
The transition property
.element {
transition: property duration timing-function delay;
}| Parameter | Description | Example |
|---|---|---|
| property | Which property to animate | opacity, transform, all |
| duration | How long it lasts | 200ms, 0.3s |
| timing-function | Speed curve | ease, linear, ease-in-out |
| delay | Wait before starting | 0ms, 100ms |
Practical example
.button {
background: #1a1a2e;
transition: background 200ms ease, transform 150ms ease;
}
.button:hover {
background: #3a3a5e;
transform: translateY(-2px);
}Multiple transitions
Separate each property with a comma. Avoid transition: all because it can animate unexpected properties and affect performance.
Timing functions
| Function | Behavior |
|---|---|
ease |
Slow start, fast middle, slow end (default) |
ease-in |
Slow start, fast end |
ease-out |
Fast start, slow end |
ease-in-out |
Slow on both ends |
linear |
Constant speed |
cubic-bezier() |
Custom curve |
For user interactions, ease-out feels more natural because it responds quickly to input.
Recommended durations
| Interaction type | Duration |
|---|---|
| Hover, focus | 150-200ms |
| Menu opening | 200-300ms |
| Page transition | 300-500ms |
| Entry animation | 400-600ms |
More than 500ms feels slow. Less than 100ms is imperceptible.
Animations with @keyframes
For more complex movements, @keyframes lets you define multiple steps:
@keyframes slide-in {
0% {
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.panel {
animation: slide-in 400ms ease both;
}Animation properties
| Property | Description |
|---|---|
animation-name |
Name of the @keyframes |
animation-duration |
Total duration |
animation-timing-function |
Speed curve |
animation-delay |
Wait before starting |
animation-iteration-count |
Number of repetitions (1, infinite) |
animation-direction |
Direction (normal, reverse, alternate) |
animation-fill-mode |
Final state (forwards, backwards, both) |
Shorthand
.element {
animation: name 400ms ease 0ms 1 normal both;
/* name dur timing delay count dir fill */
}Staggered animations
To animate a list of elements with a cascading effect, use incremented animation-delay:
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 75ms; }
.item:nth-child(3) { animation-delay: 150ms; }Or with custom properties for greater flexibility:
.item {
animation: appear 400ms ease both;
animation-delay: calc(var(--i) * 75ms);
}<li class="item" style="--i: 0">First</li>
<li class="item" style="--i: 1">Second</li>
<li class="item" style="--i: 2">Third</li>Animation performance
The browser can efficiently animate only two properties without causing repaint:
- transform (translate, scale, rotate)
- opacity
Animating width, height, margin, top, left forces the browser to recalculate the layout of the entire page (reflow), which is expensive.
Accessibility: prefers-reduced-motion
Some users experience dizziness or discomfort with animations. Respect their preference:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}Animations bring your interface to life. In the next lesson, we will learn custom properties, CSS's native variables that make your code maintainable and dynamic.
Practice
- Add transitions to a button: Create a button with transitions on
backgroundandtransformon hover. Useease-outand a duration of 200ms for a natural feel. - Create a loading spinner: Implement a circular spinner using
@keyframeswith a 360-degree rotation andanimation: spin 800ms linear infinite. - Respect prefers-reduced-motion: Add a
@media (prefers-reduced-motion: reduce)block that reduces the duration of all animations and transitions to 0.01ms.
/* Basic button transition */
.button {
background: #1a1a2e;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 200ms ease,
transform 150ms ease;
}
.button:hover {
background: #2d2d44;
transform: translateY(-2px);
}
.button:active {
transform: translateY(0) scale(0.98);
transition-duration: 50ms;
}
/* Card with multiple transitions */
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e0e0e0;
transition: border-color 200ms ease,
box-shadow 300ms ease;
}
.card:hover {
border-color: #b056ff;
box-shadow: 0 8px 24px rgb(176 86 255 / 15%);
}
/* Staggered entry transition with delay */
.list-item {
opacity: 0;
transform: translateY(10px);
animation: appear 400ms ease forwards;
}
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 75ms; }
.list-item:nth-child(3) { animation-delay: 150ms; }
.list-item:nth-child(4) { animation-delay: 225ms; }
/* Fade-in animation */
@keyframes appear {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section {
animation: appear 600ms ease both;
}
/* Loading spinner */
@keyframes spin {
to { rotate: 1turn; }
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #b056ff;
border-radius: 50%;
animation: spin 800ms linear infinite;
}
/* Notification pulse */
@keyframes pulse {
0%, 100% { scale: 1; }
50% { scale: 1.15; }
}
.badge-notification {
background: #e6286a;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
animation: pulse 2s ease-in-out infinite;
}
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Sign in to track your progress