- Structure your HTML with semantic
elements inside a scroll container withrole="region". - Apply
scroll-snap-type: x mandatoryto the container andscroll-snap-align: centerto each item. - Generate previous/next buttons using
::scroll-button(left)and::scroll-button(right)pseudo-elements. - Create dot indicators with
::scroll-marker()on each item and style the active state via:checked. - Bind entrance animations to scroll position using
animation-timeline: view(inline)withanimation-range. - Add a progress bar using a named
scroll-timelineon the container tied to a::beforepseudo-element. - Guard all new features inside
@supportsblocks so unsupported browsers fall back to a scrollable container. - Respect accessibility with
@media (prefers-reduced-motion: reduce)to disable animations.
For over a decade, building a carousel on the web meant reaching for a JavaScript library. Between 2025 and 2026, the CSS Overflow Level 5 specification and Scroll-Driven Animations Level 1 specification changed that picture entirely. This article walks through building a complete image carousel with previous/next navigation buttons, scroll marker dots, snap behavior, and scroll-driven animation effects—all in pure CSS.
Table of Contents
The End of JavaScript Carousels
For over a decade, building a carousel on the web meant reaching for a JavaScript library. Slick, Swiper, Flickity, Embla Carousel, and dozens of others filled a gap the platform could not. Each brought its own bundle weight, its own accessibility quirks, and its own event-handling overhead. Swiper v11’s full minified bundle weighs approximately 140 KB; even modular imports still land in the 40-60 KB range for core plus navigation. Multiply that across the countless marketing pages, e-commerce product galleries, and media-heavy layouts that depend on carousels, and the cost compounds: inflated Time-to-Interactive, main-thread blocking during initialization, and growing total transfer size across every page that mounts a slider.
The accessibility story was rarely better. JavaScript-driven carousels routinely broke keyboard navigation, failed to expose proper ARIA semantics, and hijacked focus in unpredictable ways. Only the most diligent library authors patched in screen reader support. Everyone else ignored it.
That picture changed between 2025 and 2026. The CSS Overflow Level 5 specification introduced ::scroll-button() and ::scroll-marker() pseudo-elements. The Scroll-Driven Animations Level 1 specification achieved broad but not yet universal engine support; Firefox support remains partial as of mid-2026. Together, these APIs make fully functional, accessible carousels possible with zero JavaScript.
Together, these APIs make fully functional, accessible carousels possible with zero JavaScript.
This article walks through building exactly that: a complete image carousel with previous/next navigation buttons, scroll marker dots, snap behavior, and scroll-driven animation effects, all in pure CSS. No libraries. No scripts. No build step.
Prerequisites and Browser Support
What You Need to Know
This tutorial assumes intermediate CSS knowledge. Readers should be comfortable with custom properties, pseudo-elements, and CSS Grid/Flexbox layouts. Familiarity with the scroll-snap family of CSS properties is helpful but not strictly required, as the article covers the fundamentals.
Current Browser Support (2026)
As of mid-2026, the scroll-driven carousel APIs enjoy strong but not universal support.
Chromium browsers (Chrome 135+, Edge 135+) ship full support for ::scroll-button(), ::scroll-marker(), ::scroll-marker-group, and animation-timeline: scroll() / view(). Safari 19+ provides full support, following WebKit’s implementation landing in early 2026. Firefox offers partial support behind flags: scroll-snap and animation-timeline work, but ::scroll-button() and ::scroll-marker() remain behind the layout.css.scroll-driven-animations.enabled flag at time of writing (verified in Firefox 130; flag name may differ in earlier or later releases). Firefox users get a functional scrollable container without the generated navigation controls.
Progressive enhancement is the correct strategy. The carousel degrades gracefully to a horizontally scrollable container in unsupported browsers. The @supports feature detection blocks shown later in this article handle that cleanly. The Can I Use pages for “CSS Scroll-Driven Animations” and “CSS scroll-snap” provide current compatibility matrices.
scroll-snap-type and scroll-snap-align (Refresher)
Scroll snapping is the foundation of any CSS carousel. Apply scroll-snap-type to a scroll container to declare the snapping axis and strictness. Apply scroll-snap-align to child elements to declare where each child should snap within the container’s viewport.
<div class="carousel">
<div class="item">1div>
<div class="item">2div>
<div class="item">3div>
<div class="item">4div>
div>
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
}
.item {
flex: 0 0 80%;
scroll-snap-align: center;
}
Setting mandatory forces the container to always rest on a snap point after scrolling completes. The center alignment means each item centers itself within the visible area. This two-property combination provides the discrete, panel-by-panel movement that defines carousel behavior.
::scroll-button(left) and ::scroll-button(right) — Native Navigation
This pseudo-element generates interactive navigation controls directly from the scroll container, with no elements in the markup. The browser creates these as accessible, focusable controls that scroll the container by one “page” in the specified direction when activated.
The pseudo-element accepts four directional values: left, right, up, and down. For a horizontal carousel, ::scroll-button(left) and ::scroll-button(right) produce previous and next buttons respectively. A content value is required for the pseudo-element to render. It accepts text characters, CSS images, or SVG data URIs.
A key behavioral detail: the browser automatically disables a scroll button when the container reaches the corresponding scroll boundary. At the start of the carousel, ::scroll-button(left) enters a disabled state. At the end, ::scroll-button(right) does the same. Per the CSS Pseudo-Elements Level 5 draft, ::scroll-button() enters a :disabled state at scroll boundaries; verify this behavior in target browser DevTools. You can style this state, eliminating the manual boundary-checking logic that JavaScript carousels typically implement.
The scroll container must have overflow: auto or overflow: scroll for the pseudo-elements to generate. Setting overflow: hidden suppresses them.
::scroll-marker-group and ::scroll-marker() — Native Dot Indicators
Apply ::scroll-marker() to children of a scroll container to generate dot indicators (or any styled marker) corresponding to each child. The parentheses in ::scroll-marker() are part of the pseudo-element function syntax; they do not accept arguments in the current specification. The browser groups these markers into a ::scroll-marker-group pseudo-element on the container itself.
Set ::scroll-marker-group to display: flex to arrange markers in a row. Adding justify-content: center centers the dot row. You control its position with standard CSS positioning or order properties.
The browser manages active state automatically. The marker corresponding to the currently snapped item receives the :checked pseudo-class. Clicking any marker scrolls the container to the associated item. Add scroll-behavior: smooth to the container to enable animated scrolling. This replaces the click handlers, intersection observers, and state management that JavaScript carousel libraries use to synchronize dot indicators with scroll position.
animation-timeline: scroll() and animation-timeline: view() — Scroll-Driven Animations
Scroll-driven animations decouple CSS animations from time and bind them to scroll progress instead. Two timeline types serve different purposes.
With animation-timeline: scroll(), an animation tracks the overall scroll progress of a container. When the container is scrolled 50% of its scrollable distance, the animation is 50% complete. This works well for progress bars and container-level effects.
With animation-timeline: view(), an animation tracks an element’s visibility within a scroll container’s viewport. As the element enters, crosses, and exits the visible area, the animation progresses. This drives per-item effects like fading, scaling, and parallax.
Use animation-range to restrict which portion of the timeline drives the animation. For instance, animation-range: entry 0% entry 100% constrains the animation to the entry phase, from the moment the element first becomes visible to the moment it is fully visible. The first visible item on page load may appear at its to state immediately, since it is already past the entry phase; consider animation-fill-mode or an initial opacity/scale rule to handle this edge case. In left-to-right writing modes, the inline keyword in view(inline) specifies the horizontal (inline) axis for horizontal scroll containers.
Step 1 — Building the HTML Structure
Semantic Markup for an Accessible Carousel
You don’t need elements for navigation or a wrapper for dots. The CSS pseudo-elements generate those controls. The markup contains only elements inside a container, with no control wrappers.
Replace all src values below with absolute image URLs before testing (for example, https://picsum.photos/seed/1/800/300 through https://picsum.photos/seed/6/800/300).
<div class="carousel" role="region" aria-label="Featured images">
<figure class="carousel-item">
<img src="https://picsum.photos/seed/1/800/300" alt="Mountain landscape at sunrise" />
<figcaption>Mountain Sunrisefigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/2/800/300" alt="Ocean waves crashing on rocks" />
<figcaption>Coastal Wavesfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/3/800/300" alt="Dense forest canopy from below" />
<figcaption>Forest Canopyfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/4/800/300" alt="Desert dunes under a clear sky" />
<figcaption>Desert Dunesfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/5/800/300" alt="Snow-covered mountain peak" />
<figcaption>Alpine Peakfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/6/800/300" alt="Wildflower meadow in spring" />
<figcaption>Spring Meadowfigcaption>
figure>
div>
The role="region" and aria-label attributes identify the carousel as a landmark for assistive technology. Each with its and provides self-contained, semantically meaningful content.
CSS Grid Carousel Track
The carousel container uses CSS Grid with grid-auto-flow: column to lay items out horizontally. Each item is sized to occupy most of the container’s width, leaving a hint of adjacent items visible to signal scrollability.
Applying Scroll Snap
Setting scroll-snap-type: x mandatory on the container ensures crisp panel-to-panel snapping. Each item’s scroll-snap-align: center centers it in the viewport after a scroll gesture completes.
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 85%;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: auto;
padding: 1rem;
position: relative;
box-sizing: border-box;
scroll-timeline: --carousel inline;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel-item {
scroll-snap-align: center;
border-radius: 0.75rem;
overflow: clip;
overflow-clip-margin: 0px;
margin: 0;
}
.carousel-item img {
width: 100%;
height: 300px;
object-fit: cover;
display: block;
}
.carousel-item figcaption {
padding: 0.75rem 1rem;
font-size: 1rem;
background: #111;
color: #fff;
}
The scrollbar-width: auto retains the native scrollbar as a navigation affordance in browsers that do not support ::scroll-button(). When those pseudo-elements are available, the scrollbar is hidden inside an @supports block (shown in the progressive enhancement section). The box-sizing: border-box declaration ensures that grid-auto-columns: 85% and padding: 1rem resolve predictably without items overflowing or being mis-sized. Using overflow: clip on .carousel-item (instead of overflow: hidden) contains visual overflow from border-radius rounding without clipping the translateX motion on child figcaption elements used for the parallax animation. The grid-auto-columns: 85% sizing ensures each item takes up 85% of the container width, leaving the edges of neighboring items visible as a scroll affordance. The scroll-behavior: smooth property ensures that programmatic and marker-triggered scrolling is animated rather than an instant jump. The scroll-timeline: --carousel inline declaration creates a named scroll timeline on the container for use by the progress bar animation.
Generating Previous/Next Buttons
Apply ::scroll-button(left) and ::scroll-button(right) directly to the .carousel container. Each requires a content property to render visibly.
Styling the Scroll Buttons
@supports selector(::scroll-button(right)) {
.carousel::scroll-button(left),
.carousel::scroll-button(right) {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 1.25rem;
display: grid;
place-content: center;
cursor: pointer;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: background 0.2s ease, opacity 0.2s ease;
}
.carousel::scroll-button(left) {
content: "◀";
left: 0.5rem;
}
.carousel::scroll-button(right) {
content: "▶";
right: 0.5rem;
}
.carousel::scroll-button(left):hover,
.carousel::scroll-button(right):hover {
background: rgba(0, 0, 0, 0.85);
}
.carousel::scroll-button(left):disabled,
.carousel::scroll-button(right):disabled {
opacity: 0.3;
cursor: default;
}
}
Per the CSS Pseudo-Elements Level 5 draft, the :disabled pseudo-class applies automatically when the carousel reaches either scroll boundary; verify in target browser DevTools that the rule activates as expected. No JavaScript boundary detection is needed. The transition on opacity and background provides visual feedback on hover and state change. The @supports selector(::scroll-button(right)) guard prevents browsers without support from encountering parse errors.
Generating Markers from Carousel Items
Each .carousel-item gets a ::scroll-marker() pseudo-element. The content property creates the visual dot.
Positioning the Marker Group
The ::scroll-marker-group pseudo-element on the container holds all generated markers and is styled as a centered flex row.
Styling Active and Inactive States
@supports selector(::scroll-marker()) {
.carousel::scroll-marker-group {
display: flex;
justify-content: center;
gap: 0.5rem;
padding-top: 1rem;
position: relative;
bottom: 0;
}
.carousel-item::scroll-marker() {
content: "";
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
border: 2px solid transparent;
transition: background 0.3s ease, transform 0.3s ease;
cursor: pointer;
}
.carousel-item::scroll-marker():checked {
background: #0070f3;
transform: scale(1.3);
border-color: #0070f3;
}
}
The :checked pseudo-class on ::scroll-marker() reflects the currently snapped item. Note the parentheses in ::scroll-marker():checked — both the () on the pseudo-element and the :checked pseudo-class are required for a valid compound selector. When a user clicks an inactive dot, the browser scrolls to the corresponding carousel item (animated when scroll-behavior: smooth is set on the container). The 300 ms transition on background and transform produces a crossfade between inactive and active dot states.
Fade and Scale on Scroll with view()
Binding animations to each item’s visibility within the scroll container creates entrance effects that respond to user interaction rather than arbitrary timing. The first item is already fully visible on page load (its entry phase is complete), so it is excluded from the scroll-driven animation and instead rendered at full opacity and scale immediately.
@keyframes fade-scale-in {
from {
opacity: 0.3;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.carousel-item:first-child {
animation: none;
opacity: 1;
transform: scale(1);
}
The view(inline) timeline tracks each item’s horizontal visibility. The animation-range: entry 0% entry 100% restricts the animation to the entry phase. As an item scrolls into view from either side, it transitions from 30% opacity and 90% scale to full visibility and size. The scroll-driven animation bindings for non-first items are applied inside the @supports guard shown in the progressive enhancement section, so browsers without support do not receive animation-timeline declarations they cannot parse.
Parallax Effect on Captions
Applying a separate view timeline to elements creates a parallax offset where captions translate at a different rate than their parent images. The .carousel-item uses overflow: clip (rather than overflow: hidden) so that the translateX motion on the caption is not clipped.
@keyframes caption-parallax {
from {
transform: translateX(-30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
The slightly delayed animation-range starting at entry 20% means the caption begins animating after the image has already started fading in, creating a staggered reveal. The animation-timeline and animation-range bindings for figcaption are applied inside the @supports guard shown in the progressive enhancement section.
Progress Bar Tied to Scroll Position
A ::before pseudo-element on the container, animated with the named --carousel scroll timeline defined on the container, creates a progress indicator that tracks overall scroll position.
.carousel::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 3px;
width: calc(100% - 2rem);
background: #0070f3;
transform-origin: left;
z-index: 20;
pointer-events: none;
}
@keyframes progress-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
The --carousel timeline references the container’s own inline-axis scroll progress (defined by scroll-timeline: --carousel inline on .carousel). As the user scrolls through the carousel, the bar scales from 0 to full width. The width: calc(100% - 2rem) accounts for the container’s left and right padding so the bar resolves against the visible port width. The pointer-events: none ensures the bar does not intercept clicks on carousel items. Because transform: scaleX() is a compositor-friendly property, this animation can run off the main thread with no layout or paint cost for compositor-eligible properties such as transform and opacity. The animation-timeline binding for ::before is applied inside the @supports guard shown in the progressive enhancement section.
The Complete Carousel — Full Code
Combined HTML
<div class="carousel" role="region" aria-label="Featured images">
<figure class="carousel-item">
<img src="https://picsum.photos/seed/1/800/300" alt="Mountain landscape at sunrise" />
<figcaption>Mountain Sunrisefigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/2/800/300" alt="Ocean waves crashing on rocks" />
<figcaption>Coastal Wavesfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/3/800/300" alt="Dense forest canopy from below" />
<figcaption>Forest Canopyfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/4/800/300" alt="Desert dunes under a clear sky" />
<figcaption>Desert Dunesfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/5/800/300" alt="Snow-covered mountain peak" />
<figcaption>Alpine Peakfigcaption>
figure>
<figure class="carousel-item">
<img src="https://picsum.photos/seed/6/800/300" alt="Wildflower meadow in spring" />
<figcaption>Spring Meadowfigcaption>
figure>
div>
Combined CSS
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 85%;
gap: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: auto;
padding: 1rem;
position: relative;
box-sizing: border-box;
scroll-timeline: --carousel inline;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 3px;
width: calc(100% - 2rem);
background: #0070f3;
transform-origin: left;
transform: scaleX(0);
z-index: 20;
pointer-events: none;
}
.carousel-item {
scroll-snap-align: center;
border-radius: 0.75rem;
overflow: clip;
overflow-clip-margin: 0px;
margin: 0;
}
.carousel-item:first-child {
opacity: 1;
transform: scale(1);
}
.carousel-item img {
width: 100%;
height: 300px;
object-fit: cover;
display: block;
}
.carousel-item figcaption {
padding: 0.75rem 1rem;
font-size: 1rem;
background: #111;
color: #fff;
}
@keyframes fade-scale-in {
from { opacity: 0.3; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
@keyframes caption-parallax {
from { transform: translateX(-30px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes progress-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@supports selector(::scroll-button(right)) {
.carousel {
scrollbar-width: none;
}
.carousel::-webkit-scrollbar {
display: none;
}
.carousel::scroll-button(left),
.carousel::scroll-button(right) {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 1.25rem;
display: grid;
place-content: center;
cursor: pointer;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: background 0.2s ease, opacity 0.2s ease;
}
.carousel::scroll-button(left) {
content: "◀";
left: 0.5rem;
}
.carousel::scroll-button(right) {
content: "▶";
right: 0.5rem;
}
.carousel::scroll-button(left):hover,
.carousel::scroll-button(right):hover {
background: rgba(0, 0, 0, 0.85);
}
.carousel::scroll-button(left):disabled,
.carousel::scroll-button(right):disabled {
opacity: 0.3;
cursor: default;
}
}
@supports selector(::scroll-marker()) {
.carousel::scroll-marker-group {
display: flex;
justify-content: center;
gap: 0.5rem;
padding-top: 1rem;
}
.carousel-item::scroll-marker() {
content: "";
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
border: 2px solid transparent;
transition: background 0.3s ease, transform 0.3s ease;
cursor: pointer;
}
.carousel-item::scroll-marker():checked {
background: #0070f3;
transform: scale(1.3);
border-color: #0070f3;
}
}
@supports (animation-timeline: view()) {
.carousel-item:not(:first-child) {
animation: fade-scale-in 1s linear both;
animation-timeline: view(inline);
animation-range: entry 0% entry 100%;
}
.carousel-item figcaption {
animation: caption-parallax 1s linear both;
animation-timeline: view(inline);
animation-range: entry 20% entry 100%;
}
.carousel::before {
animation: progress-bar 1s linear both;
animation-timeline: --carousel;
}
}
@media (prefers-reduced-motion: reduce) {
.carousel-item,
.carousel-item figcaption,
.carousel::before {
animation: none;
animation-timeline: none;
animation-range: normal;
}
}
Live Demo
To test this carousel, copy the combined HTML and CSS above into a local .html file or your preferred code playground. Drop in real image URLs (the examples above use picsum.photos placeholders), adjust grid-auto-columns for different sizing, and test across Chrome, Safari, and Firefox to see the progressive enhancement behavior firsthand.
Progressive Enhancement and Fallbacks
Feature Detection with @supports
Wrapping the newer pseudo-element styles in @supports blocks ensures that browsers without support simply render a scrollable container. The combined CSS above already places all ::scroll-button(), ::scroll-marker(), and animation-timeline rules inside their respective @supports guards. The structure follows this pattern:
@supports selector(::scroll-button(right)) {
}
@supports selector(::scroll-marker()) {
}
@supports (animation-timeline: view()) {
}
Without these features, users still get a horizontally scrollable, snap-aligned container with a visible scrollbar. The core browsing experience remains intact.
When You Still Need JavaScript
CSS carousels do not cover every use case. Autoplay with pause-on-hover requires timers that CSS cannot express. Infinite looping demands DOM manipulation or cloned nodes. You still need scripts for dynamic content loading (lazy-loaded slides fetched from an API). Complex accessibility requirements such as live region announcements when the active slide changes, or custom focus management beyond what the browser provides natively, still call for a thin JavaScript layer. The right approach is to treat these CSS APIs as the foundation and add scripting only for behaviors the platform does not yet handle.
The right approach is to treat these CSS APIs as the foundation and add scripting only for behaviors the platform does not yet handle.
Implementation Checklist
- Semantic HTML with
role="region"andaria-label scroll-snap-type: x mandatoryon containerscroll-snap-alignon each itemscroll-behavior: smoothon container for animated scrolling::scroll-button(left)and::scroll-button(right)with styled content inside@supports::scroll-marker()on items with active state via:checkedinside@supports::scroll-marker-grouppositioned and styled inside@supports- Scroll-driven animation via
animation-timeline: view()inside@supports @supportsfallbacks for unsupported browsers (scrollbar visible when buttons unavailable)- Keyboard navigation tested (Tab, Arrow keys)
- Reduced motion respected via
@media (prefers-reduced-motion: reduce)(resetsanimation,animation-timeline, andanimation-range) - Tested in Chrome, Safari, and Firefox
What This Means for Front-End Development
The pattern here is part of a broader platform trajectory. CSS nesting, :has(), container queries, and now scroll-driven animations each replace a category of logic that previously needed JavaScript or a build tool. The performance implications are direct: zero JavaScript bundle cost for carousel functionality (compared to the 40-140 KB a typical library adds), and scroll-driven animations that run on the compositor thread with GPU acceleration, avoiding main-thread layout and paint work for compositor-eligible properties such as transform and opacity.
If your carousel doesn’t need autoplay, infinite looping, or dynamic slide injection, drop the library.
Developers working with Swiper, Flickity, or Embla should evaluate whether their use cases genuinely exceed what these CSS APIs now provide natively. CSS now handles snap navigation, dot indicators, scroll-driven entrance animations, and progress tracking out of the box. If your carousel doesn’t need autoplay, infinite looping, or dynamic slide injection, drop the library. Start adopting progressively, wrap the new features in @supports, and retire the dependencies that these specifications were designed to replace.
For further reading, the W3C CSS Overflow Level 5 specification and the MDN documentation on scroll-driven animations provide the authoritative references for these APIs.

