A Confirmation Message, Two Ways
Open the IRCTC app after booking a train ticket, or any UPI app after a payment goes through, and watch closely what happens on screen. A green checkmark or a "Payment Successful" banner doesn't just materialize — it grows in, slides down, or fades into view over a fraction of a second. Now imagine the same message without that motion: one instant the screen shows a blank space, the next instant a fully formed banner is just there, as if it teleported in. Nothing is technically wrong with the second version — the information is identical — but it feels abrupt, almost broken, like a page that hasn't finished loading.
That difference is exactly what this chapter is about. In earlier CSS work you learned how to set a property to a value: color: red; or opacity: 1;. When the browser applies a plain CSS rule, it does not ease into the new value — it simply repaints the element with the new value the very next time it draws the screen. There is no "halfway red" frame. If you want the browser to draw a sequence of in-between frames — banner half-visible and half-slid-in, then three-quarters, then fully in place — you need to describe those in-between states explicitly. That is what CSS animations let you do: you write down a small number of checkpoints, called keyframes, and the browser calculates and draws every frame in between, many times a second, automatically.
Keyframes as a Timetable
Here is a concrete way to build the right mental model before we touch any syntax. Think of an express train's timetable between two stations 500 km apart, scheduled to take 5 hours. The timetable doesn't list the train's exact position every second — it lists a few checkpoints: "at 0 hours, position 0 km" and "at 5 hours, position 500 km." Everything in between is inferred: if the train travels at a constant speed, you can calculate where it is at hour 2 (200 km) or hour 3.5 (350 km) just by scaling the fraction of time elapsed against the fraction of distance covered.
A CSS @keyframes block works the same way, except the "checkpoints" are percentages of the total animation duration, and the "position" is the value of a CSS property. Suppose you're building the upload progress bar for a college-application portal, and you want the fill bar's width to grow from 0% to 100% over 5 seconds — but you also want to model something realistic: uploads often start fast and then slow down as the network gets congested. You can describe that with three checkpoints instead of two:
.progress-fill {
width: 0%;
animation: fillUp 5s linear forwards;
}
@keyframes fillUp {
0% { width: 0%; }
30% { width: 40%; }
100% { width: 100%; }
}
Read the percentages inside @keyframes as fractions of the total duration, not fractions of the property's range. The total duration here is 5 seconds (set by animation: fillUp 5s ...), so the checkpoint written as 30% actually happens at 0.30 × 5s = 1.5 seconds into the animation, and at that instant the bar's width must equal 40%. The final checkpoint, 100%, happens at the full 5 seconds, with width at 100%.
Now trace what the browser actually draws between checkpoints. From 0s to 1.5s, the width rises from 0% to 40% — that's 40 percentage points spread over 1.5 seconds, a rate of 40 ÷ 1.5 ≈ 26.7% per second. From 1.5s to 5s (a span of 3.5 seconds), the width rises from 40% to 100% — 60 percentage points over 3.5 seconds, a rate of 60 ÷ 3.5 ≈ 17.1% per second. The bar genuinely grows faster in the first stretch and slower in the second, exactly like a real upload that starts quick and then throttles. Mathematically, the width is a piecewise linear function of time — a straight line from (0s, 0%) to (1.5s, 40%), followed by a different straight line from (1.5s, 40%) to (5s, 100%). Every extra checkpoint you add to a @keyframes block adds one more straight-line segment to this function.
The Animation Property Family
The shorthand animation: fillUp 5s linear forwards; is actually packing several separate properties into one line. Knowing each one individually lets you control exactly how an animation behaves:
- animation-name: which
@keyframesblock to use — the link between the "what changes" definition and the element that should change. - animation-duration: how long one full pass through the keyframes takes, e.g.
5sor300ms. - animation-timing-function: the speed curve used within each keyframe segment — covered in detail below.
- animation-delay: how long the browser waits before starting the animation after it becomes eligible to run.
- animation-iteration-count: how many times the animation repeats — a number like
3, or the keywordinfinite. - animation-direction: whether repeats always play forward (
normal), always play backward (reverse), or alternate forward/backward each repeat (alternate). - animation-fill-mode: what the element looks like before the animation starts and after it ends — the subject of the next section, and the single most misunderstood property in this list.
- animation-play-state: lets JavaScript pause and resume a running animation via
paused/running.
Worked Example: The Cart-Badge Bump
Let's trace a complete, small animation by hand before tackling a trickier one. Many shopping apps briefly enlarge the item-count badge on the cart icon when you add something, then shrink it back — a quick "bump." Here is the CSS:
.badge {
animation: bump 0.3s ease-out;
}
@keyframes bump {
0% { transform: scale(1); }
100% { transform: scale(1.2); }
}
Trace it second by second. At t=0, the badge is at scale(1) — its normal size. Over the next 0.3 seconds, it grows smoothly toward scale(1.2) — 20% larger. At t=0.3s the animation is finished. Now ask: what size is the badge the instant after the animation ends? Notice that animation-fill-mode was never set here, and its default value is none. A fill-mode of none means: once the animation finishes, stop applying it entirely — the property goes back to being governed only by ordinary (non-animation) CSS rules, exactly as if the animation had never run. Since .badge has no other rule setting transform, its ordinary value is the CSS initial value, transform: none, which is visually identical to scale(1). So the badge doesn't just end at 1.2× size and stay there — it pops up to 1.2× and then instantly snaps back down to its normal size the moment the 0.3 seconds are up. That snap is what actually produces the "bump" feeling: grow, then pop back — not grow-and-stay.
The Vanishing Toast: Understanding animation-fill-mode
The cart badge happened to snap back to a state that was harmless — the badge's normal resting size. But the exact same fill-mode default can cause a real bug when an element's hidden state and its visible state are both meaningfully different from the CSS the browser falls back to. Consider a "toast" notification — a small banner that slides in from the left to say "Order placed" and should then stay put. Its hidden resting state and its animation are written like this:
.toast {
opacity: 0;
transform: translateX(-100%);
}
.toast.show {
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
Here, .toast by itself is invisible and pushed off-screen to the left — that's its permanent, ordinary CSS state, used whenever the .show class is absent. When some JavaScript adds the .show class (say, right after an order is confirmed), the animation property newly applies, and the browser plays slideIn over 0.5 seconds: the toast slides from off-screen to on-screen while fading from invisible to fully visible. So far this matches what a developer expects.
Now trace what happens at t=0.5s, the instant the animation completes. .toast.show still matches the element — the class was never removed — but animation-fill-mode is still at its default, none. As with the badge, "fill-mode none" means the animation stops influencing opacity and transform the moment it ends, and those properties fall back to whatever ordinary CSS rules say. The only ordinary rule that sets them is the base .toast rule — opacity: 0; transform: translateX(-100%); — because .toast.show only ever set animation, never opacity or transform directly. So at t=0.5s the toast doesn't calmly stay in place; it instantly reverts to invisible and off-screen, exactly where it started, even though the .show class is still attached. To a user, the banner slides in beautifully for half a second and then vanishes as if it had never appeared — a real, reproducible bug, not a hypothetical one.
Common misconception: many students assume that once a keyframe animation "reaches" its to state, the element simply stays there forever, because that's the last thing they watched happen on screen. That's true only when a fill-mode says so. By default, the browser treats the time outside the animation's active duration (before its delay and after it ends) as none of the animation's business — it hands control straight back to ordinary CSS, whatever that says. If ordinary CSS agrees with the to keyframe (as it does for elements with no conflicting base rule), you won't notice anything odd — which is exactly what happened with the plain opacity examples many students first try. But whenever the base, non-animated CSS differs from the final keyframe — as it deliberately does here, because the toast needs a genuine "hidden" resting state — the mismatch becomes visible as a snap or, in this case, a full vanish.
The fix is animation-fill-mode: forwards, which tells the browser: after the animation ends, keep applying the values from the final keyframe indefinitely, instead of handing control back to ordinary CSS.
.toast.show {
animation: slideIn 0.5s ease-out forwards;
}
With forwards added, at t=0.5s the browser continues treating opacity as 1 and transform as translateX(0) — the to keyframe's values — even though the animation itself has stopped running. The toast now slides in and correctly stays visible.
Shaping Time: Timing Functions
Between any two keyframes, animation-timing-function decides how the property value travels from the start value to the end value — not just what the values are, but the speed curve along the way. Think of a train pulling out of a crowded platform like Mumbai CST. It doesn't move at constant speed: it starts slow and picks up pace as it clears the platform, then, on arrival, it slows gradually before stopping rather than braking instantly.
- linear: constant speed throughout — like a conveyor belt, not a train. Equal time slices produce equal changes in value. This is what we used for the piecewise upload bar above.
- ease-in: starts slow, speeds up toward the end — like a train pulling away from a station.
- ease-out: starts fast, slows down toward the end — like a train gliding into a station and braking to a stop. This is why
ease-outis the most common choice for things "arriving" on screen, such as the toast above. - ease-in-out: slow start, fast middle, slow end — accelerate away from one station, cruise, decelerate into the next.
- ease: the default value if you omit a timing function; a gentle version of ease-in-out, slightly weighted toward slowing down at the end.
All of these are really just different mathematical curves plotted between 0 and 1 (technically defined using a Bézier curve, which you can even customize directly with cubic-bezier(x1, y1, x2, y2)), but for Grade 9 purposes, the station analogy is enough to choose the right one by feel: use ease-out for things entering the screen, ease-in for things leaving it, and linear only when you deliberately want mechanical, constant-speed motion, like a spinning loader.
Repeating and Reversing: iteration-count and direction
Some animations aren't a one-time event — a "LIVE" badge on a cricket score app, for instance, often has a small red dot that gently pulses for as long as the match is live:
.live-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #dc2626;
animation: pulse 1s ease-in-out infinite alternate;
}
@keyframes pulse {
from { opacity: 1; transform: scale(1); }
to { opacity: 0.3; transform: scale(0.8); }
}
animation-iteration-count: infinite means the animation never stops running on its own — it keeps repeating for as long as the element exists on the page. The interesting part is animation-direction: alternate. Without it, infinite alone would replay the animation identically every time: play from → to, then instantly jump back to from and play from → to again — and that jump, from a dim, small dot (opacity 0.3, scale 0.8) straight back to a bright, full-size one, would be a visible flicker at every loop boundary. With alternate, odd-numbered repeats play forward (from → to) and even-numbered repeats play backward (to → from), so the dot dims down and then brightens back up continuously, with no jump at the seams — a smooth "breathing" pulse rather than a flickering one.
Staggering with animation-delay
When a train-search results page (the kind IRCTC or any booking site shows after you search) reveals its list of matching trains, listing them all instantly in one frame looks flat; having each row fade in a beat after the previous one feels far more polished. animation-delay, applied per element, produces exactly this staggering:
.result-row {
animation: fadeInUp 0.4s ease-out both;
}
.result-row:nth-child(1) { animation-delay: 0s; }
.result-row:nth-child(2) { animation-delay: 0.1s; }
.result-row:nth-child(3) { animation-delay: 0.2s; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
Notice the shorthand ends in both rather than nothing. both combines two fill behaviors: backwards, which applies the from keyframe's values during the delay (so a row waiting its turn stays invisible and shifted down, instead of flashing at full opacity before its turn comes), and forwards, which holds the to keyframe's values afterward, exactly as with the toast. This is the same fill-mode concept from earlier, now solving a second, related problem — the flash before an animation starts, caused by the same "ordinary CSS takes over when the animation isn't active" rule, just applied to the time before animation-delay elapses instead of after animation-duration ends.
animation vs. transition: Clearing Up the Confusion
Common misconception: students who've used transition before often assume animation is just a fancier name for the same thing. They're related but solve different problems. A transition only interpolates between two states — the property's old value and its new value — and it needs an external trigger, such as :hover or a class change caused by JavaScript, to have anything to transition from and to:
.btn {
background-color: #2563eb;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #16a34a;
}
Here there is no keyframe list — CSS just watches background-color, and whenever it changes value for any reason, spreads that change smoothly over 0.2 seconds instead of applying it instantly. An animation, in contrast, can define any number of intermediate checkpoints (not just a start and end), can start automatically the moment an element appears in the DOM with no hover or class-toggle needed, and can repeat indefinitely with iteration-count: infinite — none of which a transition can do on its own. Use transition for simple two-state reactions to user interaction (hover, focus, a toggled class); reach for @keyframes and animation when you need multiple checkpoints, automatic playback, or looping.
Performance: Why transform and opacity Are the Animator's Best Friends
Every example in this chapter deliberately animated only transform and opacity, never raw position properties like left, top, or a growing width for anything other than a simple progress bar. That choice matters for smoothness, not just style. When the browser paints a frame, it goes through layout (figuring out where every box on the page sits and how big it is), paint (drawing pixels), and composite (layering the results on screen). Animating left or width forces the browser to redo layout for potentially the whole page on every single frame, because changing one box's size or position can shift its neighbors. Animating transform (which includes translate, scale, and rotate) and opacity, by contrast, can usually be handled at the composite stage alone — the browser can shift or fade an already-painted layer without recalculating anyone's layout. On a modest Android phone rendering a page with many moving elements, this is the difference between a silky animation and a visibly stuttering one. It's why the toast slid using transform: translateX() rather than animating its left property, even though both would move it sideways.
Check Your Understanding
-
An element has
animation: grow 2s;with no other animation properties set and no other CSS rule affecting itstransform. What is itstransformvalue at t=3 seconds (after the animation has finished)? Explain using fill-mode.Answer:
transform: none(equivalently, no transform). Sinceanimation-fill-modewas never set, it defaults tonone, so once the 2-second animation ends at t=2s, the browser stops applying the keyframe's values and falls back to ordinary CSS — here, the CSS initial value,transform: none. -
Using
@keyframes bump { 0% { transform: scale(1); } 100% { transform: scale(1.2); } }and.badge { animation: bump 0.3s ease-out; }, what size is the badge immediately after the animation ends, and why does this differ from a case where the base CSS and the 0% keyframe don't match?Answer: It snaps back to
scale(1)— its normal size — because the base (non-animated) CSS for.badgehas notransformrule, so it falls back to the initial valuetransform: none, which happens to equalscale(1), the animation's own0%value. This is why the bump looks like "grow, then pop back down," not "grow and stay big." -
Rewrite the toast rule from this chapter so it keeps sliding in correctly but also stays visible permanently afterward, using one added keyword.
Answer:
.toast.show { animation: slideIn 0.5s ease-out forwards; }— addingforwardstells the browser to keep applying thetokeyframe's values (opacity 1, translateX(0)) after the animation ends, instead of reverting to the base.toastrule. -
Using the
fillUpkeyframes from earlier (0% at 0s → width 0%, 30% at 1.5s → width 40%, 100% at 5s → width 100%), what is the bar's width at t=3 seconds? Show your working.Answer: t=3s falls in the second segment (1.5s to 5s). Fraction of that segment elapsed = (3 − 1.5) ÷ (5 − 1.5) = 1.5 ÷ 3.5 = 3⁄7 ≈ 0.4286. Width gained in this segment = 0.4286 × 60 percentage points ≈ 25.7%. Total width = 40% + 25.7% ≈ 65.7%.
-
A developer writes
.card:hover { transform: scale(1.05); }with atransition: transform 0.15s;on.card, and separately asks why they can't make the card "pulse three times and stop" using only this transition. What's the correct explanation, and what CSS feature would you reach for instead?Answer: A
transitiononly interpolates between an old and a new value in response to a trigger (here,:hoverstarting or ending) — it has no concept of repeating a pattern a fixed number of times on its own. To pulse three times automatically, you need@keyframesplusanimation-iteration-count: 3, which can define multiple checkpoints and repeat them a set number of times without needing a hover trigger.
Put together, an animation is really three separate decisions: what changes and when (the @keyframes checkpoints, which form a piecewise function of time), how fast it moves between those checkpoints (the timing function), and what the element looks like outside the animation's active window, before its delay and after it ends (the fill mode). Get the third one wrong, as the toast example showed, and the other two can be flawless while the whole effect still fails the moment a user actually watches it play out.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where css animations: making your website dance is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting css animations: making your website dance to at least 3 other topics you have studied.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind css animations: making your website dance, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.