Skip to main content

🎬 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 @keyframes rule using percentage stops and the from/to keywords
  • Attach an animation to an element with the animation shorthand 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-motion so 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.
graph TD A[CSS Animation] --> B["@keyframes rule
(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:

  1. The @keyframes rule β€” a named set of "poses" describing what the element looks like at points along a timeline.
  2. 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 placed along an animation timeline A horizontal timeline marked 0%, 25%, 50%, 75% and 100%, with element poses above each stop, and a labelled band beneath listing the animation properties. 0% 25% 50% 75% 100% animation properties name Β· duration Β· timing-function Β· delay Β· iteration-count Β· direction Β· fill-mode Β· play-state
Figure 1 β€” The @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:

PropertyEffectPerformance
transformMove, scale, rotate, skewβœ… Excellent (GPU)
opacityFade in / outβœ… Excellent (GPU)
color / background-colorColor shifts⚠️ Moderate (repaint)
width / height / top / leftResize / 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:

  1. animation-name
  2. animation-duration
  3. animation-timing-function
  4. animation-delay
  5. animation-iteration-count
  6. animation-direction
  7. animation-fill-mode
  8. animation-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 transform and opacity for 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: forwards when an element should stay in its end state.

⚠️ Don't

  • Animate width, height, top, or left when a transform would do β€” they force costly reflows.
  • Loop attention-grabbing motion forever; it becomes noise and hurts readability.
  • Sprinkle will-change on everything β€” it consumes memory and can backfire.
  • Forget the reduced-motion fallback.

Appropriate durations

Kind of motionDurationExamples
Micro-interaction100–300msButton press, toggle
State transition200–500msPanel open, content swap
Entrance / exit300–800msModal, page transition
Attention1–2sNotification, 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

  1. Start from the HTML below. It gives you a logo, a notification badge, a spinner, and a button.
  2. Write an @keyframes rule 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.
  3. Apply each with the animation shorthand. Experiment with different durations and timing functions.
  4. 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 @keyframes rule (the poses) plus animation properties (the playback controls).
  • Keyframe stops use percentages (or from/to); the browser interpolates the frames between them.
  • The animation shorthand's first time value is duration, the second is delay β€” order matters.
  • Prefer transform and opacity; they animate on the GPU without triggering reflow.
  • Always ship a prefers-reduced-motion fallback.

πŸ“š 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.