Tie CSS animations to scroll position with animation-timeline: scroll() and view(). No scroll event listeners, no requestAnimationFrame, no IntersectionObserver.
animation-timeline: scroll() links an animation’s progress to the scroll position of a container.
animation-timeline: view() links it to an element’s visibility in the viewport.
animation-range controls when the animation starts/ends (e.g., entry 0% to entry 100%).
All runs on the compositor thread — 60fps guaranteed with zero JavaScript.
The blue bar at the top of the page fills as you scroll down. Uses animation-timeline: scroll(root) — zero JavaScript. Bootstrap ScrollSpy would need scroll event listeners + offset calculations + class toggling for a similar indicator.
Cards fade in and slide up as they enter the viewport. Uses animation-timeline: view() with animation-range: entry.
This card animates into view using only CSS. No IntersectionObserver, no scroll event listener, no animation library.
Scroll-driven animations run on the compositor thread, guaranteeing 60fps even on busy pages. JS scroll handlers run on the main thread and can jank.
Bootstrap ScrollSpy uses scroll events + offset math + class toggling (~43 KB JS). Scroll-driven animations do the same with a few CSS lines.
animation-range: entry 0% entry 100% means the animation plays during the element’s entry into the viewport. You can also use exit, contain, or cover.
scroll() tracks the scroll position of a container. view() tracks an element’s visibility within its scrolling ancestor. Both are pure CSS.
All animations are wrapped in @supports (animation-timeline: scroll()). Unsupported browsers see static content — no broken layouts.
The background moves at a different rate than the page scroll, creating a depth effect. Uses animation-timeline: view() with animation-range: entry 0% exit 100% — the shift happens as this section enters and exits the viewport.
This background shifts as you scroll. Pure CSS — no JS parallax library.
Dots and progress bar below track your horizontal scroll position. Uses named view-timeline on each slide, timeline-scope to hoist them to the wrapper, and scroll-timeline for the progress bar. Zero JavaScript.
Scroll horizontally to see the indicator dots and progress bar update in real-time.
Each dot’s opacity and scale are driven by its corresponding card’s view-timeline.
Uses named view-timeline + timeline-scope to connect dots outside the scroller to items inside it.
Compare to Bootstrap Carousel indicators which need JS to sync active state.
Zero JavaScript. Zero IntersectionObserver. Zero scroll event listeners. Just CSS.
@supports (animation-timeline: scroll()) {
/* Reading progress bar — fills as page scrolls */
.reading-progress {
position: fixed; top: 0; left: 0;
width: 0%; height: 4px;
animation: grow-width linear;
animation-timeline: scroll(root);
}
/* Reveal on scroll — cards fade in on viewport entry */
.reveal-card {
animation: reveal-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* Parallax — background shifts as section scrolls through viewport */
.parallax-inner {
animation: parallax-shift linear;
animation-timeline: view();
animation-range: entry 0% exit 100%;
}
@keyframes parallax-shift {
from { transform: translateY(-80px) scale(1.15); }
to { transform: translateY(80px) scale(1.15); }
}
/* Horizontal scroll indicator — named timelines + timeline-scope */
.hscroll-item:nth-child(1) { view-timeline: --slide-1 inline; }
.hscroll-item:nth-child(2) { view-timeline: --slide-2 inline; }
/* ... one per slide ... */
.hscroll-wrapper {
timeline-scope: --slide-1, --slide-2, --slide-3, --slide-4, --slide-5;
}
.scroll-indicator-dot:nth-child(1) { animation-timeline: --slide-1; }
.scroll-indicator-dot:nth-child(2) { animation-timeline: --slide-2; }
/* ... dot animation tracks its slide's visibility ... */
/* Progress bar tracks horizontal scroll position */
.hscroll-container { scroll-timeline: --hscroll inline; }
.hscroll-progress {
animation: grow-width linear;
animation-timeline: --hscroll;
}
}