CSS Animations
Introduction
Transitions are great for simple state changes, but some interactions require multi-step motion: loading indicators, attention pulses, entry effects, and looping decorative feedback. CSS animations provide that control with @keyframes. This chapter explains how to build clear, lightweight animations that improve UX without overwhelming users.
Prerequisites
- Basic transitions and transforms
- Familiarity with pseudo-classes and interaction states
- Basic awareness of motion accessibility concerns
Transition vs Animation
Quick rule:
- Transition: animate from one state to another when a property changes
- Animation: run a keyframed timeline that can loop or play independently
Use animations when you need multiple stages or repeated motion.
@keyframes Basics
Define animation steps:
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}Code explanation:
fromis equivalent to0%tois equivalent to100%- browser interpolates values between these keyframes
Then apply with animation.
Applying an Animation
.card {
animation: fade-in-up 320ms ease-out;
}Shorthand order typically includes:
- name
- duration
- timing function
- delay (optional)
- iteration count (optional)
- direction/fill/play-state (optional)
Core Animation Properties
Longhand controls:
animation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-fill-modeanimation-play-state
Equivalent example:
.pulse {
animation-name: pulse;
animation-duration: 1.2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}Multi-Step Keyframes
Use percentage keyframes for richer motion:
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.04);
opacity: 0.85;
}
}This creates a smooth, repeating pulse effect.
Iteration Count
.once {
animation: fade-in-up 300ms ease-out 1;
}
.loop {
animation: pulse 1.2s ease-in-out infinite;
}Guideline:
- one-time for entrances/feedback
- infinite only for meaningful continuous states (loading, recording, attention)
Animation Direction
Useful direction values:
normalreversealternatealternate-reverse
Example:
.breathing {
animation: pulse 1.4s ease-in-out infinite alternate;
}alternate avoids abrupt jump between loop cycles.
Fill Mode
animation-fill-mode controls style before/after animation.
.toast-enter {
animation: fade-in-up 260ms ease-out forwards;
}Code explanation:
forwardskeeps final keyframe styles after animation ends- useful for entrance states where end style should persist
Other values include backwards and both.
Delays and Stagger
Small delays can create staggered appearance:
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 60ms; }
.item:nth-child(3) { animation-delay: 120ms; }Use subtle staggering for lists/cards; avoid long delays that block comprehension.
Play State Control
.spinner {
animation: spin 1s linear infinite;
}
.spinner.paused {
animation-play-state: paused;
}Useful when pausing UI motion during certain app states.
Common Animation Patterns
1) Loading Spinner
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #cbd5e1;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}2) Fade-in Entrance
.panel {
animation: fade-in-up 280ms ease-out;
}3) Attention Pulse (sparingly)
.badge-live {
animation: pulse 1.4s ease-in-out infinite;
}Use only for truly important signals.
Animating Multiple Properties
Keep animation focused on cheap, smooth properties where possible:
transformopacity
Avoid heavy frequent animation of:
- layout properties (
width,height,top,left) - complex filters on large surfaces
This improves perceived smoothness and battery usage.
Tip
Prefer Subtle Motion
Short, meaningful motion improves clarity. Constant or large movement often harms focus.
Accessibility: Reduced Motion
Always respect user motion preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}If full disable is too aggressive for your UI, at least remove non-essential decorative loops.
Common Mistakes
| Mistake | Why it hurts | Better approach |
|---|---|---|
| Infinite animation everywhere | Cognitive overload and distraction | Reserve loops for meaningful states |
| Long entrance animations | Interface feels slow | Keep most UI motion short (150-400ms) |
| Animating layout-heavy properties | Jank/performance issues | Favor transform/opacity |
| No reduced-motion fallback | Accessibility problem | Add prefers-reduced-motion handling |
| Using motion without UX purpose | Visual noise | Tie animation to state/feedback meaning |
Debugging Animations
When animation is not behaving correctly:
- verify
animation-namematches keyframes name exactly - check duration is not
0s - inspect whether another rule overrides
animation - confirm keyframe property values are animatable
- test with DevTools animation inspector/timeline
For state-driven animations, also verify class/state toggles occur as expected.
Mini Exercise
- Create a spinner with infinite
spin - Create a card entrance animation (
fade-in-up) - Add stagger delay to a 4-item list
- Add one pulse badge for live status
- Implement reduced-motion fallback and verify behavior
FAQ
Should I use transitions or keyframes for hover?
Usually transitions. Keyframes are better for multi-stage or looping motion.
Why does my animation run once and then reset?
Because default fill mode is none. Use animation-fill-mode: forwards if final state should persist.
Can CSS animations run without JavaScript?
Yes. CSS alone can handle many common effects, especially state and loop-based visual feedback.
Is infinite always bad?
No, but it should be intentional. Loading indicators and live-state signals are valid use cases.
What duration feels natural for UI animations?
Often around 150-400ms for entrances and micro-interactions, depending on motion distance and context.