Transitions and animations
transition vs @keyframes, easing, what's cheap to animate, prefers-reduced-motion.
Updated
What transitions and animations are
Motion in an interface has a job, it's not decoration: it shows what changed, where an element comes from and that your action had an effect. CSS gives you two tools:
| Transition | Animation (@keyframes) |
|
|---|---|---|
| Starts | when a property changes (hover, a new class) | on its own, on appearance, or in a loop |
| States | from A to B | any number of steps (0%, 50%, 100%) |
| When | hover, focus, open / close, feedback | loaders, page entrances, attention cues |
Transition
.btn {
transition: background-color 150ms ease, transform 150ms ease;
}
.btn:hover {
background-color: var(--accent-hover);
transform: translateY(-1px);
}transition: <property> <duration> <easing> <delay>. Avoid transition: all — it also animates properties you don't want (and it costs).
@keyframes and animation
@keyframes fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.toast {
animation: fade-in 200ms ease-out both;
}| Property | Role |
|---|---|
animation-duration |
how long it lasts |
animation-timing-function |
the speed curve |
animation-iteration-count |
how many times (infinite for a loader) |
animation-fill-mode: both |
keeps the start style before and the end style after |
Easing — the speed curve
| Easing | Feel | When |
|---|---|---|
ease-out |
starts fast, slows down | elements that appear — the most natural |
ease-in |
starts slow, speeds up | elements that disappear |
ease-in-out |
slow – fast – slow | moving from one place to another |
linear |
constant | rotations, progress, loaders |
Durations: 100–200 ms for feedback (hover), 200–300 ms for entrances, rarely over 400 ms. Long animations make the interface feel slow.
What to animate — performance
Animate only transform and opacity: they're computed on the GPU, without layout. width, height, top, margin trigger layout on every frame → a choppy animation. The explanation is in How the browser renders.
| Instead of | Use |
|---|---|
left: 0 → 100px |
transform: translateX(100px) |
width: 0 → 100% (a progress bar) |
transform: scaleX(0 → 1) |
display: none ↔ block |
opacity + visibility, or @starting-style |
Respect the user's preference
Some people get motion sickness or have vestibular sensitivity and turn on "reduce motion" in their system. Mandatory:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}In Tailwind: motion-safe:animate-… / motion-reduce:transition-none.
Modern: appearing from display: none
.dialog[open] {
opacity: 1;
transition: opacity 200ms, display 200ms allow-discrete;
@starting-style { opacity: 0; }
}@starting-style defines the "before appearing" state — you can animate elements that just switched from display: none. For transitions between pages there's the View Transitions API.
Going further
For animations in React (Motion), timelines and scroll (GSAP) and complete examples, see the Animations topic.
Summary
- Transition = from A to B on a change; animation =
@keyframes, starts on its own. ease-outon entrance, 150–300 ms; onlytransformandopacity.prefers-reduced-motionis mandatory.