The animation Property and Sub-properties
You have three @keyframes rules in style.css — fadeIn, slideUpFade, and pulse — and so far none of them do anything. A @keyframes rule is a definition. The animation property is what applies it to an element and controls every aspect of playback: how long it runs, when it starts, how many times it repeats, and what the element looks like before and after.
The eight sub-properties
Section titled “The eight sub-properties”animation-name
Section titled “animation-name”The name of the @keyframes rule to apply. Must match exactly, including case:
.hero-heading { animation-name: fadeIn;}The default is none — no animation.
animation-duration
Section titled “animation-duration”How long one complete cycle of the animation takes. Accepts s (seconds) or ms (milliseconds). There is no useful default — the browser defaults to 0s, which means the animation completes instantly and appears not to run at all. Always set a duration explicitly.
.hero-heading { animation-name: fadeIn; animation-duration: 600ms;}animation-timing-function
Section titled “animation-timing-function”Controls the rate of change between keyframe steps — the easing. The same keyword values used by transition-timing-function apply here:
| Value | Behaviour |
|---|---|
ease | Starts slow, accelerates, ends slow (default) |
linear | Constant speed throughout |
ease-in | Starts slow, ends fast |
ease-out | Starts fast, ends slow |
ease-in-out | Slow at both ends, faster in the middle |
cubic-bezier(x1,y1,x2,y2) | Custom curve |
steps(n) | Jumps between n discrete states — no interpolation |
Entrance animations almost always feel better with ease-out — they arrive quickly and decelerate into place, which reads as natural.
animation-delay
Section titled “animation-delay”How long the browser waits before starting the animation. Useful for sequencing multiple elements:
.hero-heading { animation-delay: 0ms; }.hero-subheading { animation-delay: 150ms; }.hero-cta { animation-delay: 300ms; }animation-delay also accepts negative values. A delay of -200ms tells the browser to start the animation as if it had already been running for 200ms — useful for syncing an animation to a specific mid-cycle position without waiting.
animation-iteration-count
Section titled “animation-iteration-count”How many times the animation runs. Accepts any positive number or the keyword infinite:
/* Run twice, then stop */animation-iteration-count: 2;
/* Loop forever */animation-iteration-count: infinite;
/* Run 1.5 cycles — stops at the 50% keyframe */animation-iteration-count: 1.5;animation-direction
Section titled “animation-direction”Controls which direction the keyframe timeline runs each cycle:
| Value | Behaviour |
|---|---|
normal | 0% → 100% every cycle (default) |
reverse | 100% → 0% every cycle |
alternate | 0% → 100% on odd cycles, 100% → 0% on even cycles |
alternate-reverse | 100% → 0% on odd cycles, 0% → 100% on even cycles |
alternate is the natural choice for looping animations that should reverse smoothly. The pulse keyframe you wrote — with matching 0% and 100% values — produces the same seamless loop using normal, because the end state matches the start state.
animation-play-state
Section titled “animation-play-state”Whether the animation is currently running or paused:
.card:hover .card-icon { animation-play-state: paused;}paused freezes the animation at its current position. The browser remembers where it was — removing the paused state resumes from that point. This is useful for hover-to-pause interactive controls.
animation-fill-mode
Section titled “animation-fill-mode”animation-fill-mode controls what styles apply to the element outside the animation’s active period — before it starts (during a delay) and after it ends. This is the sub-property that confuses most people, so it gets extra attention.
To understand it, imagine a timeline:
[delay period] → [animation plays] → [after animation]By default, the element’s own CSS styles apply during the delay and after the animation ends. The keyframe values only apply while the animation is actively running.
none (default)
The element uses its own styles both before and after. If fadeIn runs on an element whose own styles include opacity: 1, the element is visible during the delay, disappears to opacity: 0 the instant the animation starts, fades back to 1, and then pops back to the element’s own opacity: 1 after it ends. Usually not what you want.
forwards
After the animation ends, the element keeps the styles from the final keyframe (100%) rather than reverting to its own styles. Use this for entrance animations — the element should stay visible after fading in, not snap back to its pre-animation state.
.hero-heading { animation-name: fadeIn; animation-duration: 600ms; animation-fill-mode: forwards;}backwards
During the delay period, the element uses the styles from the first keyframe (0%) rather than its own styles. Without this, an element with a delay is visible at its normal styles until the animation starts — then it jumps to the 0% keyframe and begins. With backwards, it starts invisible for the entire delay, then fades in smoothly. Essential for staggered entrances.
both
Applies backwards during the delay and forwards after the animation ends. For most entrance animations with a delay, both is the correct value.
.hero-heading { opacity: 0; /* Without fill-mode, you'd need this */ animation-name: slideUpFade; animation-duration: 600ms; animation-delay: 150ms; animation-fill-mode: both; /* Invisible during delay, stays in place after */}With animation-fill-mode: both, you do not need to set opacity: 0 on the element itself — the 0% keyframe handles it during the delay, and the 100% keyframe state is preserved after.
The shorthand
Section titled “The shorthand”The eight sub-properties can be combined into a single animation declaration. The order matters:
animation: name duration timing-function delay iteration-count direction fill-mode play-state;/* Longhand */.hero-heading { animation-name: slideUpFade; animation-duration: 600ms; animation-timing-function: ease-out; animation-delay: 150ms; animation-iteration-count: 1; animation-direction: normal; animation-fill-mode: both; animation-play-state: running;}
/* Equivalent shorthand */.hero-heading { animation: slideUpFade 600ms ease-out 150ms 1 normal both running;}In practice you rarely need to specify all eight. Omit any value that matches its default and the shorthand stays readable:
/* Common pattern for a one-shot entrance */.hero-heading { animation: slideUpFade 600ms ease-out 150ms both;}The one parsing rule to remember: when the shorthand contains two time values, the browser always assigns the first to duration and the second to delay. If you only provide one time value, it is duration.
Running multiple animations on one element
Section titled “Running multiple animations on one element”Separate multiple animation declarations with a comma. Each animation runs independently:
.notification-badge { animation: fadeIn 300ms ease-out both, pulse 1.2s ease-in-out 300ms infinite;}The badge fades in over 300ms, then begins pulsing indefinitely after a 300ms delay (so the pulse starts once the fade-in is complete).
Exercise
Section titled “Exercise”In style.css, find the @keyframes slideUpFade rule you wrote in Lesson 02. Below your @keyframes block, add a test class and apply the animation using the shorthand:
.animate-test { animation: slideUpFade 600ms ease-out both;}Temporarily add class="animate-test" to a heading or paragraph in index.html and open it in the browser. You should see the element fade in and slide up from 24px below its natural position.
Then experiment with animation-fill-mode by changing both to each of the other values in turn:
none— watch the element appear at its natural position, then animate. After the animation ends, it stays — but try addinganimation-iteration-count: 3and watch what happens at the end.forwards— remove any explicitopacityon the element and confirm it stays visible after the animation ends.backwards— addanimation-delay: 800msand confirm the element is invisible during the delay.both— confirm the element is invisible during the delay and stays in its animated position after.
Remove class="animate-test" from the HTML when you are done. You are not building the permanent STO animations yet — that starts in Lesson 05.
animation-namenames the@keyframesrule to apply.animation-durationis required — the default is0sand the animation will not appear to run without it.animation-timing-functioncontrols easing.ease-outis the most natural choice for entrance animations.animation-delayaccepts negative values to start mid-cycle.animation-iteration-count: infiniteloops indefinitely.animation-direction: alternatereverses direction on even cycles — useful for smooth back-and-forth loops.animation-fill-modeis the most commonly misunderstood sub-property.bothis the correct value for entrance animations with a delay:backwardsapplies the 0% keyframe during the delay,forwardskeeps the 100% keyframe after the animation ends.- In the shorthand, the first time value is always
duration, the second is alwaysdelay. - Multiple animations on one element are separated by commas.
Lesson 04 steps back from the STO project and builds a toolkit of reusable animation patterns — spinner, pulse, skeleton shimmer, slide-in — that you can take into any project.