Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate+200 XP

CSS Animations & Keyframes

@keyframes and the animation property in full — building genuinely custom motion beyond simple hover transitions.

40 min August 2026
// Part 01 — Transitions vs Animations

When Two States Aren't Enough

The Transitions module covered smooth movement between exactly two states — a resting state and a triggered state (like :hover). A CSS animation, defined with @keyframes, removes that two-state limit entirely: it can define any number of intermediate steps, run automatically without needing a trigger like hover or a class toggle, and loop indefinitely.

Something a transition genuinely cannot do — a continuous loading spinner
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}
/* No hover, no class toggle, no trigger of any kind — this runs
   continuously the instant the element exists in the DOM */
// Part 02 — @keyframes Syntax

Defining the Steps of the Motion

from/to — the two-keyframe shorthand
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
Percentage keyframes — for any number of intermediate steps
@keyframes pulse {
  0%   { transform: scale(1);   opacity: 1;   }
  50%  { transform: scale(1.1); opacity: 0.7; }
  100% { transform: scale(1);   opacity: 1;   }
}

from/to are exactly equivalent to 0%/100% — the percentage form is required the moment you need any intermediate step beyond just a start and end point, as the pulse example above shows.

// Part 03 — The animation Property

Every Sub-Property, Explained

The full set of animation properties
.element {
  animation-name: pulse;                 /* which @keyframes block to use */
  animation-duration: 2s;                /* one full cycle's length */
  animation-timing-function: ease-in-out; /* the pacing curve within one cycle */
  animation-iteration-count: infinite;    /* or a specific number like 3 */
  animation-direction: alternate;         /* normal / reverse / alternate / alternate-reverse */
  animation-delay: 0.5s;                  /* wait before starting */
  animation-fill-mode: forwards;          /* what state to hold before/after running */
}

/* The shorthand — same six values, one line */
.element {
  animation: pulse 2s ease-in-out infinite alternate 0.5s forwards;
}

animation-direction: alternate is what makes the pulse example above breathe in and out smoothly rather than snapping back to the start at the end of every cycle — it reverses the keyframe playback on every other iteration instead of always playing forward from 0% to 100%.

// Part 04 — animation-fill-mode

The Property That Confuses Almost Everyone Once

By default, an element snaps back to its original, pre-animation styles the instant the animation finishes (or before it starts, during any animation-delay) — animation-fill-mode controls whether the animation's first or last keyframe state is instead held onto outside its active running time.

Without fill-mode — the element snaps back after finishing
@keyframes slide-in {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}

.card {
  animation: slide-in 0.5s ease-out;
  /* After 0.5s, the element reverts to whatever transform it had
     BEFORE the animation — often back to no transform at all,
     undoing the slide-in visually the instant it "finishes" */
}
With fill-mode: forwards — the final keyframe state is kept
.card {
  animation: slide-in 0.5s ease-out forwards;
  /* The element stays at transform: translateX(0) permanently
     after the animation completes, exactly as intended */
}
⚠️ Important
Forgetting animation-fill-mode: forwards on a one-shot "reveal" animation is one of the single most common real CSS animation bugs. The animation visually appears to run then instantly undo itself, because without forwards, the browser reverts every animated property back to its pre-animation value the moment the animation ends.
// Part 05 — When to Reach for Keyframes vs a Transition

A Clear Decision, Not a Style Preference

Transition — two states, triggered by something
.button {
  transition: background-color 0.2s;
}
.button:hover {
  background-color: #4285f4;
}
Keyframes — needs multiple steps, or runs without a trigger
@keyframes attention-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-6px); }
  75%      { transform: translateX(6px); }
}
.error-field {
  animation: attention-shake 0.4s ease-in-out;
}

The decision is genuinely mechanical: if the motion is a simple change between exactly two states and something (hover, a class change) triggers it, a transition is simpler and sufficient. The moment you need more than two states, need the motion to loop, or need it to run automatically without an external trigger, only @keyframes can do it.

// Part 06 — Performance, Revisited

The Same Cheap-vs-Expensive Rule Still Applies

Everything from the Transitions module about which properties animate cheaply (transform, opacity — GPU-accelerated, no layout recalculation) versus expensively (width, height, top/left — trigger layout reflow on every single frame) applies identically inside @keyframes. An animation looping indefinitely on an expensive property is a much more serious, sustained performance cost than a one-off transition, since it runs continuously rather than just once.

Expensive — animating left continuously
@keyframes slide-loop {
  from { left: 0; }
  to   { left: 100px; }
}
/* Every single frame recalculates layout for this element AND
   potentially its siblings — running "infinite" makes this cost ongoing */
Cheap — the same visual result, via transform
@keyframes slide-loop {
  from { transform: translateX(0); }
  to   { transform: translateX(100px); }
}
/* GPU-composited, no layout recalculation on any frame */
// Part 07 — Real World
💼 What This Looks Like at Work

A "Broken" Toast Notification, at a Nashville Fintech Startup

Scenario — Fintech startup, Nashville · UI animation bug

A "payment successful" toast notification is built with a slide-in-and-fade-in animation. QA reports that the toast appears to flash on screen instantly for a single frame, then vanish, before its animation has even visually started.

The animation as originally written
@keyframes toast-in {
  from { transform: translateY(-20px); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}

.toast {
  animation: toast-in 0.3s ease-out;
}

What was actually happening

Without animation-fill-mode: forwards, the browser reverted the toast's transform and opacity back to their pre-animation CSS values the instant the 0.3-second animation completed — and since the element's base (non-animated) styles had opacity: 1 and no transform at all, the toast briefly animated in, then immediately snapped to a DIFFERENT, also-fully-visible state, reading as a confusing flicker rather than a clean, held final position. Adding forwards to the animation shorthand fixed it entirely — the engineer's own note afterward: "the animation was working correctly the whole time. It just had nowhere to land."

// Part 08 — Misconceptions

Four Misconceptions About CSS Animations

"transitions and @keyframes animations are just two different syntaxes for the same capability"
A transition only interpolates between exactly two states and needs an external trigger (hover, a class change). @keyframes supports any number of intermediate steps and can run automatically, looping indefinitely, with no trigger at all — a transition genuinely cannot do either of those things.
"An animation automatically stays at its final visual state once it finishes"
By default, the element snaps back to its pre-animation styles the instant the animation ends — animation-fill-mode: forwards is required to hold the final keyframe state, exactly the bug shown in the Real World example.
"animation-direction: alternate reverses the CSS PROPERTY VALUES, not just the timing"
It reverses the PLAYBACK direction of the same keyframes on alternating iterations (0%→100% then 100%→0%) — it does not change what the keyframes themselves declare, just which direction through them each cycle runs.
"Since keyframe animations often run continuously, their performance cost doesn't matter as much as a one-off transition's"
The opposite is true — an animation set to infinite runs its cost on every single frame for as long as the element exists, making the cheap-vs-expensive property distinction (transform/opacity vs width/height/top/left) MORE consequential for animations than for a one-time transition, not less.
// Part 09 — Interview Prep

5 Interview Questions — With Complete Answers

When would you reach for @keyframes instead of a simple transition?
When the motion needs more than two states (multiple intermediate keyframe steps), needs to run automatically without an external trigger like hover or a class toggle, or needs to loop — a transition can only interpolate between exactly two states and always needs a trigger.
What does animation-fill-mode: forwards do, and why is it so commonly needed?
It holds the animation's final keyframe state permanently after the animation completes, instead of the default behaviour of reverting to the element's pre-animation styles. It's commonly needed for any "reveal" or "enter" animation meant to leave the element in its animated-in state rather than snapping back.
What does animation-direction: alternate actually change?
It reverses the PLAYBACK direction of the keyframes on every other iteration (forward, then backward, then forward again) rather than always restarting from 0% — producing a smooth back-and-forth motion instead of an abrupt reset at the end of every cycle.
Why does an infinitely-looping animation on the width property cost more than the same visual effect done via transform?
width is a layout-affecting property — animating it triggers a layout recalculation on every single frame, for as long as the animation runs. transform is GPU-composited and does not trigger layout at all, making it dramatically cheaper, especially for an animation set to run continuously.
What is the difference between the from/to and percentage-based @keyframes syntax?
from/to are exactly equivalent to 0%/100% — a shorthand for a simple two-point animation with no intermediate steps. Percentage syntax is required the moment an animation needs any number of intermediate keyframe steps beyond just a start and end point.
// Common Mistakes

Animation Mistakes Beginners Make Constantly

Forgetting animation-fill-mode: forwards on a one-shot reveal animation
The animation appears to run and then instantly undo itself, since the element reverts to its pre-animation styles the moment the animation completes — exactly the flickering toast bug from the Real World example.
Animating width/height/top/left in a looping @keyframes block
Every frame of an infinitely-running animation on a layout-affecting property triggers a full layout recalculation — a real, ongoing performance cost. Use transform for the equivalent visual movement instead.
Using @keyframes for a simple two-state hover effect
A plain transition is simpler and sufficient whenever the motion is genuinely just two states triggered by something — reaching for the more complex @keyframes tool adds unnecessary complexity for no real benefit.
Setting animation-iteration-count: infinite without considering the ongoing performance and battery cost
A continuously running animation, especially on mobile, has a real ongoing cost — reserve infinite loops for cases where the continuous motion is genuinely necessary (a loading spinner), not decorative flourishes.
// Error Library

Issues You Will Hit With CSS Animations — And Exactly Why

An animation appears to run once and then immediately snap back to its original state
Cause: animation-fill-mode was left at its default value, so the browser reverts every animated property back to its pre-animation value the instant the animation completes.
Fix: Add forwards to the animation shorthand (or set animation-fill-mode: forwards explicitly) to hold the final keyframe state.
An @keyframes animation never runs at all, with no console error
Cause: A typo in the animation-name (or the shorthand animation property) that does not exactly match the name declared in the @keyframes rule — CSS silently ignores a reference to a keyframes block that does not exist, with no error of any kind.
Fix: Double-check the animation name matches exactly (case-sensitive) between the @keyframes declaration and the animation property referencing it.
An animation runs noticeably choppy/janky, especially on mobile
Cause: The animated property (commonly width, height, top, left, or margin) triggers layout recalculation on every single frame — a real, measurable performance cost that compounds when the animation runs continuously.
Fix: Rewrite the animation to use transform and/or opacity for the equivalent visual effect wherever geometrically possible — both are GPU-composited and avoid layout recalculation entirely.

🎯 Key Takeaways

  • @keyframes defines any number of steps and can run automatically without a trigger, looping indefinitely — capabilities a two-state transition simply does not have.
  • The animation shorthand combines name, duration, timing-function, iteration-count, direction, delay, and fill-mode in one line.
  • animation-fill-mode: forwards is required to hold an animation's final state — without it, the element reverts to its pre-animation styles the instant the animation completes, a genuinely common source of flickering UI bugs.
  • animation-direction: alternate reverses keyframe playback on alternating cycles, producing smooth back-and-forth motion instead of an abrupt reset.
  • Choose a plain transition for a simple, triggered two-state change; reach for @keyframes only once you need more steps, looping, or automatic playback.
  • The cheap-vs-expensive property distinction from Transitions applies with MORE consequence to animations, since a looping animation pays its cost on every frame for as long as it runs.

What comes next

Module 33 covers CSS transforms in 2D and 3D — translate, rotate, scale, skew, and building real perspective-based depth.

Module 33 → CSS Transforms (2D and 3D)
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...