CSS Animations & Keyframes
@keyframes and the animation property in full — building genuinely custom motion beyond simple hover transitions.
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.
@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 */Defining the Steps of the Motion
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}@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.
Every Sub-Property, Explained
.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%.
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.
@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" */
}.card {
animation: slide-in 0.5s ease-out forwards;
/* The element stays at transform: translateX(0) permanently
after the animation completes, exactly as intended */
}forwards, the browser reverts every animated property back to its pre-animation value the moment the animation ends.A Clear Decision, Not a Style Preference
.button {
transition: background-color 0.2s;
}
.button:hover {
background-color: #4285f4;
}@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.
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.
@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 */@keyframes slide-loop {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
/* GPU-composited, no layout recalculation on any frame */A "Broken" Toast Notification, at a Nashville Fintech Startup
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.
@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."
Four Misconceptions About CSS Animations
5 Interview Questions — With Complete Answers
Animation Mistakes Beginners Make Constantly
Issues You Will Hit With CSS Animations — And Exactly Why
🎯 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)Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.