Scroll-driven animations
A regular animation runs by the clock: two seconds — and it is over. But its progress can be tied not to time but to scrolling: scroll the page halfway — the animation is halfway through, scroll back — it rewinds. This used to be written in JavaScript; now one CSS property is enough.
A scroll timeline: scroll()
The animation-timeline property sets the timeline — what drives the animation. The scroll() function links it to the scrolling of the nearest scrollable block. Let us make a reading progress bar — a strip that grows as the text is scrolled:
<style>
@keyframes grow {
from { scale: 0 1; }
to { scale: 1 1; }
}
.reader { height: 180px; overflow-y: auto; border: 1px solid silver; }
.progress {
position: sticky;
top: 0;
height: 6px;
background: tomato;
transform-origin: left;
animation: grow linear;
animation-timeline: scroll();
}
.reader p { margin: 12px; }
</style>
<div class="reader">
<div class="progress"></div>
<p>Scroll this text — the red strip at the top shows how much you have read.</p>
<p>Espresso is made under pressure: hot water passes through tightly packed ground coffee in about half a minute.</p>
<p>A cappuccino is espresso, milk and thick milk foam in roughly equal parts.</p>
<p>A latte has more milk and a thin foam, so it tastes milder.</p>
<p>A flat white is made with a double espresso and a thin layer of velvety milk.</p>
<p>You have read to the end — the strip is full.</p>
</div>No duration is given: with a scroll timeline, its role is played by the path from the start to the end of scrolling. On your own page give the bar position: fixed — then scroll() finds the scrolling of the whole page.
A view timeline: view()
The view() function follows not the whole scroll but how the element itself passes through the visible area. This is how blocks are revealed on scroll:
<style>
@keyframes reveal {
from { opacity: 0; translate: -60px 0; }
to { opacity: 1; translate: 0 0; }
}
.list { height: 200px; overflow-y: auto; padding: 8px; border: 1px solid silver; }
.item {
margin-bottom: 12px;
padding: 24px 12px;
background: lavender;
animation: reveal linear both;
animation-timeline: view();
animation-range: entry;
}
</style>
<div class="list">
<div class="item">Cappuccino</div>
<div class="item">Latte</div>
<div class="item">Mocha</div>
<div class="item">Flat white</div>
<div class="item">Espresso</div>
<div class="item">Americano</div>
</div>Each card slides in from the left while it appears from below. Scroll back — the animation plays in reverse. both in animation keeps the first frame before the start and the last one after the end.
The range: animation-range
animation-range chooses on which part of the element's path the animation runs. In the example above entry means while the card is entering the visible area:
| Range | When the animation runs |
|---|---|
cover | the whole time the element is at least partly visible (default) |
contain | while the element is fully visible |
entry | while the element is entering the visible area |
exit | while the element is leaving it |
Percentages can be added: entry 20% cover 50% — from 20% of the entry to the middle of the path.
Support and a fallback
Scroll-driven animations work in Chrome and Edge since 2023, Safari added them later; check current support, especially in Firefox, on caniuse.com. If elements appear from transparency, in an old browser they may stay invisible. So such styles are wrapped in @supports:
@supports (animation-timeline: view()) {
.item {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry;
}
}Do not forget prefers-reduced-motion from the lesson "Animations" either: for people who turned off motion, simply showing the content is enough.
Common mistake
animation-timeline before animation. The animation shorthand resets all its parts, the timeline included, back to time. An animation with a duration of 0 ends instantly: the bar is full at once and does not change:
<style>
@keyframes grow { from { scale: 0 1; } to { scale: 1 1; } }
.reader { height: 120px; overflow-y: auto; border: 1px solid silver; }
.bar { position: sticky; top: 0; height: 6px; background: crimson; transform-origin: left;
animation-timeline: scroll();
animation: grow linear; }
.reader p { margin: 12px; height: 200px; }
</style>
<div class="reader">
<div class="bar"></div>
<p>Scroll — the bar does not change.</p>
</div>Write animation first, and animation-timeline and animation-range after it.