π¬ Keyframe Animation Fundamentals
Motion is a language. Used well, it guides attention, confirms actions, and makes an interface feel alive. In this lesson you'll learn the engine behind almost every native web animation β the CSS @keyframes rule β and how to drive it with the animation property to build fades, spins, bounces, and more.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Write an
@keyframesrule using percentage stops and thefrom/tokeywords - Attach an animation to an element with the
animationshorthand in the correct value order - Choose animatable properties (transform, opacity) that keep motion smooth
- Build five reusable effects β fade, pulse, slide, bounce, spin β from scratch
- Honor
prefers-reduced-motionso your animations stay accessible
Estimated Time: 30β40 minutes β’ Difficulty: Intermediate
Hands-on: Build a four-effect animation showcase (logo, badge, spinner, button) with a reduced-motion fallback.
In This Lesson
Animations vs. Transitions
You've already met CSS transitions, which smoothly interpolate a property between two states when something changes β a hover, a class toggle, a focus. Transitions are perfect for simple "A to B" moves, but they have limits: they need a trigger, they only describe a start and an end, and they can't loop on their own.
CSS animations lift all three limits. An animation can:
- run on its own, the moment an element appears β no trigger required;
- pass through many intermediate states, not just start and end;
- repeat, reverse, and alternate automatically, forever if you want.
π‘ A useful analogy: A transition is a light switch on a dimmer β flip it and the room fades from dark to bright. An animation is a pre-programmed light show: it can pulse, cycle through colors, and loop all night without anyone touching the switch.
(the choreography)"] A --> C["animation properties
(the playback controls)"] B --> D[States at 0%, 50%, 100%...] C --> E[Duration & delay] C --> F[Iteration & direction] C --> G[Fill mode & play state]
The Anatomy of an Animation
Every CSS animation is built from exactly two parts that work together:
- The
@keyframesrule β a named set of "poses" describing what the element looks like at points along a timeline. - The animation properties β applied to the element to say which keyframes to play and how to play them (how long, how many times, which direction).
Think of it like a flip book. The @keyframes are the individual drawings that define position at key moments; the animation properties decide how fast you flip through them and whether you flip back to the start when you reach the end.
@keyframes rule defines poses along the timeline; the animation properties decide how the browser plays through them.Writing @keyframes
An @keyframes rule has a name and one or more stops. Each stop is written as a percentage of the total duration, and inside it you list the CSS properties the element should have at that moment. The browser interpolates β smoothly fills in β everything between your stops.
@keyframes grow-in {
0% {
opacity: 0;
transform: scale(0.5); /* start small and invisible */
}
50% {
opacity: 1;
transform: scale(1.2); /* overshoot past full size */
}
100% {
opacity: 1;
transform: scale(1); /* settle to normal size */
}
}
Here the element fades in while growing, briefly overshoots at the halfway point, then settles β a lively, "poppy" entrance.
Percentages, or just from / to
The stops are progress markers through the animation's duration:
- 0% (or
from) β the starting pose; - 100% (or
to) β the ending pose; - anything in between β an intermediate pose.
When you only need a start and an end, from/to reads more clearly:
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
You can also give several stops the same pose by comma-separating the percentages β invaluable for effects like a bounce that returns to the ground several times.
Which properties can you animate?
Most properties with a numeric (or color) value can be animated. The ones you'll reach for most are:
| Property | Effect | Performance |
|---|---|---|
transform | Move, scale, rotate, skew | β Excellent (GPU) |
opacity | Fade in / out | β Excellent (GPU) |
color / background-color | Color shifts | β οΈ Moderate (repaint) |
width / height / top / left | Resize / reposition | β Costly (reflow) |
π Key Terms
Interpolation: the browser's automatic calculation of every in-between frame from your keyframe stops.
Reflow (layout): recalculating the size and position of elements β expensive, triggered by animating width, height, top, left, etc.
Composite: the cheapest stage of rendering; transform and opacity can often be handled here by the GPU alone.
β οΈ Some properties can't animate
A property with no meaningful "halfway" value β like display, which is either on or off β cannot be smoothly animated. To fade an element out and then remove it from layout, animate opacity and switch display at the end (via animationend in JS, or the newer @starting-style and transition-behavior: allow-discrete features).
Applying the Animation
Defining @keyframes does nothing on its own β you must attach it to an element. At minimum you need a name and a duration:
.badge {
animation-name: fade-in;
animation-duration: 2s; /* one cycle takes 2 seconds */
}
Without a duration the animation defaults to 0s and jumps straight to the final state with nothing visible. The name is case-sensitive and must match the @keyframes exactly.
The animation shorthand
In practice you'll almost always use the animation shorthand, which packs every sub-property into one declaration:
.badge {
/* name duration timing delay count direction fill-mode */
animation: fade-in 2s ease-in-out 0.5s infinite alternate forwards;
}
The canonical order of the eight values is:
animation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-fill-modeanimation-play-state
π‘ One rule to remember about time values
The first time value the browser reads becomes the duration; the second becomes the delay. So animation: fade-in 2s 0.5s; means "2-second animation, start after 0.5s" β order matters, but you only need to include the values you actually want. The rest fall back to their defaults.
Five Essential Recipes
These five patterns cover the vast majority of real UI motion. Learn them and you can build most interface animations by combining and tweaking.
1. Fade In
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.modal { animation: fade-in 0.4s ease-out; }
Like gently turning up the lights in a dark room. Ideal for modals, toasts, and freshly loaded content.
2. Pulse (attention)
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.cta:hover { animation: pulse 1s ease-in-out infinite; }
A subtle "beating heart" that draws the eye to a call-to-action without shouting.
3. Slide In From Left
@keyframes slide-in-left {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
.sidebar { animation: slide-in-left 0.5s ease-out forwards; }
A drawer opening from off-screen. forwards keeps it in place at the end instead of snapping back. Note we animate translateX, not left β much smoother.
4. Bounce
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-30px); }
60% { transform: translateY(-15px); }
}
.notification-icon { animation: bounce 2s ease; }
A ball bouncing with decreasing height. The comma-separated stops share the "grounded" pose, and the two peaks (40% then a smaller 60%) create the settling rhythm.
5. Spin (loader)
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-color);
border-left-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
A classic loading indicator. linear keeps the rotation perfectly even, and infinite runs it until you remove the animation. The trick is the single colored border side sweeping around a transparent ring.
Combining recipes
Need a badge that pops into view and keeps pulsing? Stack animations with commas:
.badge {
animation:
grow-in 0.3s ease-out forwards,
pulse 1.5s ease-in-out 0.3s infinite;
}
Accessibility & Best Practices
Respect prefers-reduced-motion
Some people experience nausea, dizziness, or migraines from screen motion (vestibular disorders). Every animated interface must offer a calm alternative. Browsers expose the user's OS-level setting through a media query:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This near-instant override effectively disables motion while still leaving elements in their final state. It's like offering both stairs and an elevator β everyone reaches the same floor by the route that suits them.
Do / Don't
β Do
- Animate
transformandopacityfor buttery 60fps motion. - Keep UI feedback short β 100β300ms for micro-interactions.
- Give animations a purpose: feedback, orientation, or drawing attention.
- Add
animation-fill-mode: forwardswhen an element should stay in its end state.
β οΈ Don't
- Animate
width,height,top, orleftwhen atransformwould do β they force costly reflows. - Loop attention-grabbing motion forever; it becomes noise and hurts readability.
- Sprinkle
will-changeon everything β it consumes memory and can backfire. - Forget the reduced-motion fallback.
Appropriate durations
| Kind of motion | Duration | Examples |
|---|---|---|
| Micro-interaction | 100β300ms | Button press, toggle |
| State transition | 200β500ms | Panel open, content swap |
| Entrance / exit | 300β800ms | Modal, page transition |
| Attention | 1β2s | Notification, achievement |
Too slow feels sluggish; too fast goes unnoticed. Like conversation, motion has a natural, comfortable pace.
Hands-on Exercise
ποΈ Build an Animation Showcase
Objective: Practice defining and applying @keyframes across four different elements, then add a reduced-motion fallback.
Instructions
- Start from the HTML below. It gives you a logo, a notification badge, a spinner, and a button.
- Write an
@keyframesrule for each: the logo should fade in and grow, the badge should bounce, the spinner should rotate continuously, and the button should pulse on hover. - Apply each with the
animationshorthand. Experiment with different durations and timing functions. - Add a
@media (prefers-reduced-motion: reduce)block that disables the looping animations.
Starter HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation Showcase</title>
<style>
body { font-family: system-ui, sans-serif; display: grid;
place-items: center; min-height: 100vh; gap: 2rem; }
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; }
.logo { width: 90px; height: 90px; border-radius: 50%;
background: #3498db; color: #fff; display: grid;
place-items: center; font-weight: 700; }
.badge { width: 44px; height: 44px; border-radius: 50%;
background: #e74c3c; color: #fff; display: grid; place-items: center; }
.spinner { width: 44px; height: 44px; border: 5px solid #eee;
border-top-color: #9b59b6; border-radius: 50%; }
.btn { padding: 12px 24px; background: #2ecc71; color: #fff;
border: none; border-radius: 6px; cursor: pointer; }
/* 1. Define your @keyframes here */
/* 2. Apply the animations here */
/* 3. Add your prefers-reduced-motion block here */
</style>
</head>
<body>
<div class="grid">
<div class="logo">LOGO</div>
<div class="badge">3</div>
<div class="spinner"></div>
<button class="btn">Hover Me</button>
</div>
</body>
</html>
π‘ Hint
The spinner and button both loop, so they need infinite. The logo should end in its final state, so add forwards. Attach the button's pulse under .btn:hover so it only runs on hover.
β Solution (CSS)
@keyframes grow-in {
from { opacity: 0; transform: scale(0.6); }
to { opacity: 1; transform: scale(1); }
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-18px); }
60% { transform: translateY(-9px); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
.logo { animation: grow-in 0.6s ease-out forwards; }
.badge { animation: bounce 2s ease; }
.spinner { animation: spin 1s linear infinite; }
.btn:hover { animation: pulse 0.9s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.logo, .badge, .spinner, .btn:hover {
animation-duration: 0.01ms;
animation-iteration-count: 1;
}
}
π― Quick Quiz
Question 1: What is the minimum you must specify for an animation to be visible?
Question 2: Which pair of properties gives the smoothest, GPU-friendly animations?
Question 3: Why should every animated site include a prefers-reduced-motion block?
Summary & Quiz
π Key Takeaways
- A CSS animation = an
@keyframesrule (the poses) plus animation properties (the playback controls). - Keyframe stops use percentages (or
from/to); the browser interpolates the frames between them. - The
animationshorthand's first time value is duration, the second is delay β order matters. - Prefer
transformandopacity; they animate on the GPU without triggering reflow. - Always ship a
prefers-reduced-motionfallback.
π Further Reading
π What's Next?
Now that you can define and apply animations, the next lesson dives into the eight animation properties in depth β mastering timing functions, cubic-bΓ©zier curves, direction, fill modes, and running several animations on one element.
π Nice work!
You can now make things move on purpose. Next we'll make them move with precision.