Animations
Learn to define multi-step animations with @keyframes and control them with the animation-* properties, and understand key performance and accessibility considerations.
Transitions only animate between two states — a start and an end. When you need something more elaborate — a multi-step sequence, a looping effect, an animation that plays automatically without user interaction — you need CSS animations, built from @keyframes plus a set of animation-* properties that control how that keyframe sequence plays.
@keyframes: defining the sequence
A @keyframes rule names an animation sequence and defines what the element should look like at various points along it, expressed as percentages from 0% (the start) to 100% (the end). You can also use the keywords from and to as shorthand for 0% and 100% when you only need two steps.
Inside each percentage, you write normal CSS property declarations describing that step's appearance. The browser automatically interpolates (smoothly animates) between each defined step for you — you don't need to define every single frame, just the key ones.
Applying an animation: the animation-* properties
Defining @keyframes alone does nothing — you have to attach that named sequence to an element using animation-name, matching the name given in the @keyframes rule. Alongside it:
• animation-duration — how long one full cycle of the animation takes • animation-delay — how long to wait before starting • animation-iteration-count — how many times to repeat; a number, or the keyword infinite for endless looping • animation-direction — normal (always forward), reverse (always backward), alternate (forward then backward, alternating each cycle), alternate-reverse • animation-fill-mode — what styles apply before the animation starts / after it ends: none (default, reverts to base styles), forwards (keeps the final keyframe's styles after finishing), backwards (applies the first keyframe's styles during the delay), both • animation-play-state — running (default) or paused; useful for pausing an animation via JavaScript or on :hover • animation-timing-function — the same ease/linear/cubic-bezier pacing curve concept from transitions, applied per-cycle (or per-keyframe-segment)
The animation shorthand
As with transitions, these are usually combined: animation: slideIn 0.5s ease-out 0.2s 1 forwards; — name, duration, timing-function, delay, iteration-count, fill-mode, in roughly that conventional order (direction and play-state can also be included in the shorthand, though they're less commonly needed).
Performance: what's cheap to animate, and what's expensive
Not all CSS properties are equally cheap to animate. Animating transform and opacity is generally very efficient because modern browsers can often hand these off to the GPU as pure compositing operations — moving or fading a pre-rendered layer — without recalculating the page's layout or repainting pixels underneath.
By contrast, animating properties like width, height, top, left, or margin typically forces the browser to recompute layout (reflow) on every single frame, because changing these properties can shift the position and size of other elements on the page. Doing this 60 times a second for a smooth animation is significantly more expensive and can cause visible jank (stuttering) especially on lower-powered devices.
The practical takeaway: wherever a design goal can be achieved with transform (translate/scale/rotate) instead of position or size properties, prefer the transform-based approach for animation.
Accessibility: respecting prefers-reduced-motion
As introduced in the media queries lesson, some users need reduced motion for genuine physical comfort or health reasons. Any nontrivial CSS animation — especially large-scale movement, spinning, or flashing — should be wrapped or overridden for users who've set that preference: @media (prefers-reduced-motion: reduce) { .el { animation: none; } } (or shorten durations drastically rather than removing them entirely, if the animation conveys necessary information).
Keyframes and the animation shorthand
animation-fill-mode: forwards is required if you want the element to keep the LAST keyframe's styles after the animation finishes, instead of snapping back to its original CSS. infinite for animation-iteration-count loops forever — combine carefully with reduced-motion overrides.
@keyframes slideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1); }
}
.toast {
animation: slideIn 0.4s ease-out forwards;
}
.notification-dot {
animation: pulse 1.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.toast, .notification-dot {
animation: none;
}
}Code Examples
The badge scales up and fades slightly at the midpoint (50%), then returns to its normal size and opacity at 100% — and because 0% and 100% are identical, the loop is seamless with no visible jump between cycles.
animation: pulse 2s ease-in-out infinite;
forwards keeps the heading fully visible and in place after the one-time animation completes rather than reverting. The reduced-motion override disables the animation entirely and applies the final resting state directly, so the content is still fully visible immediately for those users.
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
}
.badge {
animation: pulse 2s ease-in-out infinite;
}@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-heading {
animation: fadeUp 0.6s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.hero-heading {
animation: none;
opacity: 1;
transform: none;
}
}Cheap vs. expensive properties to animate
Prefer transform and opacity for anything animated frequently or continuously.
CHEAP (compositor only) EXPENSIVE (triggers layout)
transform width / height
opacity top / left / right / bottom
│ margin / padding
▼ │
GPU handles it, ▼
no layout recalculation browser recalculates layout
on every animation frameImportant Rules
@keyframes only defines a sequence by name — an element must reference it via animation-name (or the shorthand) to actually animate.
animation-fill-mode: forwards keeps the final keyframe's styles after the animation ends; without it, styles revert to the element's base CSS.
Prefer animating transform and opacity over width/height/top/left for smoother, more efficient animation.
Always provide a prefers-reduced-motion override for nontrivial or looping animations.
animation-iteration-count: infinite loops forever — make sure this is intentional and provide a way to reduce or stop it for accessibility.
Where this matters in production interfaces
Loading spinners, toast notifications sliding in, skeleton-loading shimmer effects, attention-drawing pulse badges on notification icons, and onboarding walkthroughs all commonly use @keyframes animations rather than simple transitions, because they involve multi-step or looping motion that a two-state transition can't express. Performance-conscious teams specifically audit these for transform/opacity usage, since janky animations are one of the most noticeable quality signals users perceive, especially on mobile hardware.
@keyframes grow { 0% { width: 100px; } 100% { width: 300px; } } .box { animation: grow 1s; }
@keyframes grow { 0% { transform: scaleX(0.33); } 100% { transform: scaleX(1); } } .box { animation: grow 1s; transform-origin: left; }
Animating width forces a layout recalculation on every frame. An equivalent visual effect can often be achieved with transform: scale(), which the browser can composite far more cheaply..toast { animation: slideIn 0.4s ease-out; } /* expects final position to persist */.toast { animation: slideIn 0.4s ease-out forwards; }
Without animation-fill-mode: forwards, once the animation finishes the element reverts to its original (pre-animation) CSS styles, which often looks like the element snapping back or disappearing unexpectedly.
[Choose the Correct Syntax] Which animation shorthand correctly makes an element fade in over 0.5 seconds, wait 0.2 seconds before starting, and keep its final (fully visible) appearance after finishing rather than reverting? A. animation: fadeIn 0.5s 0.2s; B. animation: fadeIn 0.5s ease 0.2s 1 forwards; C. animation: fadeIn 0.2s ease 0.5s 1 reverse; D. animation: fadeIn 0.5s ease 0.2s infinite backwards;
B. animation: fadeIn 0.5s ease 0.2s 1 forwards; — Option 2 correctly orders duration (0.5s) before delay (0.2s), plays once (the default iteration-count of 1 is fine to state explicitly), and uses forwards to retain the animation's final state. Option 4 uses infinite, which would loop forever rather than playing once and staying visible, and backwards (not forwards) applies the FIRST keyframe during the delay, not the final state after completion.
Create a @keyframes animation named spin that rotates an element a full 360 degrees, and apply it to a .loader element so it spins continuously and smoothly forever at a constant, unchanging speed.
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .loader { animation: spin 1s linear infinite; }
A constant, unchanging speed calls for the linear timing function specifically, not ease. Infinite looping needs animation-iteration-count: infinite (or the shorthand equivalent).