Loading...

CSS Animations

Transitions taught A → B on a trigger. Transforms taught where the painted box goes. Animations write a timeline: pulse a status dot, spin a loader forever, stagger cards as they enter — with @keyframes for the steps and animation to run them.

What an animation is

An animation has two parts: a named keyframe set (@keyframes) that lists styles at percentages (or from / to), and a rule that plays that name with a duration, optional delay, iteration count, direction, timing function, and fill mode. The browser interpolates between keyframe stops.

Name matching is strict: animation-name: pulse only runs if @keyframes pulse exists. Rename one without the other and nothing moves.

Animation controls at a glance

📊 Core animation pieces

Piece Role
@keyframes name { … } Timeline of property values at % / from / to
animation-name Which keyframe set to play
animation-duration How long one cycle lasts (s or ms)
animation-timing-function Easing inside the cycle (ease, linear, …)
animation-delay Wait before the first cycle
animation-iteration-count How many cycles (number or infinite)
animation-direction normal, reverse, alternate, alternate-reverse
animation-fill-mode Styles before/after (none, forwards, backwards, both)
animation shorthand name → duration → timing → delay → iteration → direction → fill-mode (common order)

You rarely need every longhand at once. Start with name + duration; add infinite for loaders, alternate for bounce-back, and forwards when the last keyframe should stick.

First timeline — a calm pulse

Opacity and transform stay paint-friendly. A soft pulse is a classic first @keyframes exercise.

Soft status pulse with @keyframes

css
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.55;
transform: scale(0.92);
}
}

.status-dot {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background: #16a34a;
animation: pulse 1.4s ease-in-out infinite;
}

0% and 100% match so the loop joins cleanly. infinite keeps the cycle going — fine for a small status indicator, noisy if you animate a whole page hero.

Infinite spin — loaders love linear

Rotation with linear and infinite reads as a continuous spinner. Pair with a reduced-motion fallback that hides or freezes the spin.

Loader ring with infinite rotate

css
@keyframes spin {
to {
transform: rotate(360deg);
}
}

.loader {
width: 2rem;
height: 2rem;
border: 3px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
.loader {
animation: none;
border-top-color: #e5e7eb;
}
}

to is shorthand for 100%. Starting at the default 0% (no transform) is enough when you only care about the end of the turn.

Delay and stagger — same keyframes, different starts

One keyframe set can drive many elements. Stagger with animation-delay so a list feels sequential without writing three different animations.

Staggered fade-up on list items

css
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(0.75rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.reveal-list li {
opacity: 0;
animation: fade-up 0.45s ease forwards;
}

.reveal-list li:nth-child(1) { animation-delay: 0s; }
.reveal-list li:nth-child(2) { animation-delay: 0.08s; }
.reveal-list li:nth-child(3) { animation-delay: 0.16s; }

forwards keeps the final keyframe styles after the cycle ends — without it, items can snap back to opacity: 0 from the base rule. That is animation-fill-mode doing real work.

Fill-mode on a one-shot entrance

Entrance motions often run once. Fill-mode decides whether styles apply before the delay and after the last frame.

Card entrance with fill-mode both

css
@keyframes card-in {
from {
opacity: 0;
transform: translateY(1rem) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

.card-enter {
animation: card-in 0.5s ease 0.1s both;
}

Shorthand here: name card-in, duration 0.5s, easing ease, delay 0.1s, fill-mode both. both applies the first keyframe during the delay and holds the last keyframe afterward.

Direction, play state, and when not to animate

animation-direction: alternate makes odd cycles run forward and even cycles reverse — handy for a gentle bounce without duplicating keyframes. animation-play-state: paused freezes a running animation (useful on hover pause for decorative loops).

For a simple hover color change, keep using transitions. Reach for animations when you need loops, multi-step stories, or motion that is not tied to a single state toggle. Prefer transform and opacity inside keyframes; animating width, height, or top is heavier and often jitters layout.

Try it yourself

  1. Build a pulse with three stops (0% / 50% / 100%) on opacity only; then add a tiny scale.
  2. Make a spinner with spin + linear + infinite; switch to ease and notice the uneven feel.
  3. Stagger three badges with the same fade-up keyframes and delays of 0s, 0.1s, 0.2s — compare with and without forwards.
  4. Use shorthand animation: card-in 0.5s ease 0.15s both on a panel; remove both and watch the snap-back.
  5. Under prefers-reduced-motion: reduce, set animation: none (or a single short fade) on decorative loops.

Common mistakes

  • Expecting a hover A→B fade to “loop” — that is animation territory, not a lonely transition
  • Typo mismatch between animation-name and @keyframes name — silent no-op
  • infinite on large, attention-heavy regions without a pause or reduced-motion path
  • Animating layout properties (width, margin, top) when transform/opacity would do
  • Omitting forwards / both on one-shot reveals that start from opacity: 0 in the base rule

Summary

  • @keyframes define the timeline; animation (or longhands) play it
  • Use iteration, direction, delay, and fill-mode for loops, bounce-back, stagger, and sticky ends
  • Prefer transform/opacity; honor prefers-reduced-motion on decorative motion
  • Transitions for simple state changes; animations for multi-step and repeating motion
  • Next in Effects & Motion: CSS Filters

🧠 Test Your Knowledge

Ready to Start

Test Your Knowledge

Challenge yourself with this interactive quiz and see how well you understand the topic

❓
7
Questions
🎯
70%
To Pass
♾️
∞
Time
🔄
∞
Attempts

📝 Instructions

  • Read each question carefully
  • Select the best answer for each question
  • You can retake the quiz as many times as you want
  • Your progress will be shown at the top