CSS animations let you transition an element between multiple states over time — without JavaScript. They are smoother than JS-driven animations for visual effects, better for performance when done correctly, and supported in every modern browser.
This guide covers how CSS animations work from the ground up: the @keyframes rule, every animation property, timing functions, common ready-to-use patterns, and the performance rules you must follow.
How CSS animation works
A CSS animation has two parts that work together:
- @keyframes — defines what changes happen and when during the animation
- animation properties — applied to the element to tell it which keyframes to use, how long to run, and how to behave
/* Step 1: Define the keyframes */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* Step 2: Apply to an element */ .modal { animation: fadeIn 0.3s ease forwards; }
@keyframes syntax
Inside @keyframes, you use percentage values to define the state of the element at each point in the animation. from is an alias for 0% and to is an alias for 100%.
@keyframes slideUp { 0% { transform: translateY(20px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } } /* Multiple stops */ @keyframes bounce { 0% { transform: translateY(0); } 30% { transform: translateY(-20px); } 60% { transform: translateY(-10px); } 80% { transform: translateY(-4px); } 100% { transform: translateY(0); } } /* Same styles at multiple stops */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
All animation properties
| Property | What it does | Example value |
|---|---|---|
| animation-name | Which @keyframes to use | fadeIn |
| animation-duration | How long one cycle takes | 0.3s |
| animation-timing-function | Speed curve of the animation | ease, linear |
| animation-delay | Wait before starting | 0.2s |
| animation-iteration-count | How many times to repeat | 1, infinite |
| animation-direction | Forward, reverse, or alternate | normal, alternate |
| animation-fill-mode | State before/after animation runs | forwards, both |
| animation-play-state | Pause or run the animation | running, paused |
The shorthand
All properties can be written in one line. The order that matters: duration must come before delay.
/* name | duration | easing | delay | iterations | direction | fill-mode */ animation: slideUp 0.4s ease-out 0.1s 1 normal forwards; /* Minimal — name and duration are required */ animation: fadeIn 0.3s; /* Multiple animations separated by commas */ animation: fadeIn 0.3s ease, slideUp 0.4s ease-out;
Timing functions explained
The timing function controls how the animation accelerates and decelerates through its duration.
| Value | Behaviour | Best for |
|---|---|---|
| ease | Slow start, fast middle, slow end | Most UI animations — default |
| linear | Constant speed throughout | Spinning loaders, progress bars |
| ease-in | Slow start, fast end | Elements leaving the screen |
| ease-out | Fast start, slow end | Elements entering the screen |
| ease-in-out | Slow start and end | Repositioning elements |
| cubic-bezier() | Custom curve with 4 control points | Brand-specific motion feel |
| steps(n) | Jumps in discrete steps | Sprite animations, typewriter effect |
/* Custom cubic-bezier — use cubic-bezier.com to generate */ animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* springy overshoot */ /* Steps — typewriter effect */ @keyframes typing { from { width: 0; } to { width: 100%; } } .typewriter { overflow: hidden; white-space: nowrap; animation: typing 2s steps(30) forwards; }
fill-mode — the most misunderstood property
By default, an element snaps back to its original style when an animation ends. animation-fill-mode controls what happens before and after.
/* none (default) — element returns to original state after animation */ animation-fill-mode: none; /* forwards — element keeps the final keyframe state */ animation-fill-mode: forwards; /* backwards — applies the first keyframe during the delay period */ animation-fill-mode: backwards; /* both — applies backwards before and forwards after */ animation-fill-mode: both;
Most of the time you want forwards or both. Using none causes a visible snap-back at the end of the animation, which looks broken on entrance effects like fade-in or slide-up.
Common ready-to-use patterns
Fade in on load
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .page-content { animation: fadeIn 0.4s ease both; }
Slide up entrance
@keyframes slideUp { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .card { animation: slideUp 0.35s ease-out both; }
Infinite spinning loader
@keyframes spin { to { transform: rotate(360deg); } } .spinner { width: 24px; height: 24px; border: 3px solid #2d3148; border-top-color: #7c6af7; border-radius: 50%; animation: spin 0.7s linear infinite; }
Pulsing skeleton loader
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .skeleton { background: linear-gradient(90deg, #1a1d27 25%, #2d3148 50%, #1a1d27 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px; }
Staggered list entrance
@keyframes fadeSlide { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .list-item { animation: fadeSlide 0.3s ease both; } /* Delay each item progressively */ .list-item:nth-child(1) { animation-delay: 0s; } .list-item:nth-child(2) { animation-delay: 0.05s; } .list-item:nth-child(3) { animation-delay: 0.1s; } .list-item:nth-child(4) { animation-delay: 0.15s; }
Performance — what you can and cannot animate
Not all CSS properties are equal when it comes to animation performance. The browser has to do very different amounts of work depending on what you change.
| Property | Performance | Why |
|---|---|---|
| transform | ✅ Excellent | Handled by GPU — no layout recalculation |
| opacity | ✅ Excellent | GPU compositing only |
| filter | 🟡 Good | GPU but heavier than transform/opacity |
| color / background | 🟡 Acceptable | Triggers repaint but not layout |
| width / height | 🔴 Avoid | Triggers full layout recalculation |
| top / left / margin | 🔴 Avoid | Triggers layout — use transform instead |
Rule: Animate only transform and opacity whenever possible. To move an element, use transform: translate() — never animate top, left, or margin.
will-change
For complex animations, you can hint to the browser that an element will be animated so it can prepare ahead of time:
.animated-element { will-change: transform, opacity; } /* Remove it after animation completes to free GPU memory */ .animated-element.done { will-change: auto; }
Do not overuse will-change. Applying it to many elements at once consumes extra GPU memory and can hurt performance rather than help it. Only use it on elements that are about to animate.
Accessibility — respecting user preferences
Some users have vestibular disorders or motion sensitivity and set their operating system to reduce motion. Always respect this preference using the prefers-reduced-motion media query.
@keyframes slideUp { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .card { animation: slideUp 0.35s ease-out both; } /* Disable or simplify animations for users who prefer it */ @media (prefers-reduced-motion: reduce) { .card { animation: fadeIn 0.1s ease both; /* simple fade instead of motion */ } }
Generate CSS animations visually
Our CSS Animation Generator lets you build keyframe animations in real time and copy the exact code — no manual writing needed.
Open Animation Generator →