Scroll-linked animations used to mean IntersectionObserver,
scroll event listeners, or a library like GSAP. Now CSS handles it
natively with animation-timeline.
You write a normal @keyframes animation, then tell the
browser to drive it with scroll progress instead of time:
@keyframes progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: currentColor;
transform-origin: left;
animation: progress linear;
animation-timeline: scroll();
}
That's a reading progress bar. No JavaScript. As the user scrolls down the
page, the bar fills from left to right. scroll() defaults to
the nearest scrollable ancestor, which for a fixed element is the
document.
view() ties the animation to when an element enters and exits
the viewport:
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
Each .card fades in as it enters the viewport.
animation-range controls which portion of the element's
visibility drives the animation. entry 0% is when the element
first appears at the bottom edge, entry 100% is when it's
fully visible.
@keyframes parallax {
from {
transform: translateY(0);
}
to {
transform: translateY(-50px);
}
}
.hero-image {
animation: parallax linear;
animation-timeline: scroll();
animation-range: 0% 50%;
}
The image shifts up slower than the scroll, creating a depth effect. No
scroll listeners, no requestAnimationFrame.
IntersectionObserver for reveal-on-scrollThe browser handles the animation on the compositor thread, so it stays smooth even on heavy pages. No layout thrashing, no jank.
Browser support is solid in Chrome and Edge. Firefox and Safari are catching up. For non-supporting browsers, the element just appears without animation, which is a fine fallback.