CSS Scroll-Driven Animations Playground

Visually manipulate structural element tracking windows to interpolate frame nodes into copy-paste scroll-timeline rules.

Browser Notice: Your browser doesn't support animation-timeline natively. The canvas preview uses a JavaScript fallback. For the native experience, use Chrome 115+ or Edge 115+. See the Polyfill tab for production deployment options.
100% Client-Side. All animation generation and code export runs entirely in your browser. No data is sent to any server — your keyframe configs stay private.
Live Preview Canvas
Timeline Mode
view()
animation-timeline type
Scroll Progress
0%
container scroll offset
Elements
1
animated targets
Scroll inside to preview
CSS native

1 What Are CSS Scroll-Driven Animations?

CSS Scroll-Driven Animations is a W3C specification that fundamentally reimagines how animations are linked to time. Rather than tying animation progress to a wall-clock timer (milliseconds elapsed), this API allows developers to bind the progress of any standard @keyframes animation directly to the scroll position of a container or the intersection of an element with its viewport — using only CSS, with zero JavaScript.

Before this API, achieving scroll-linked effects required complex JavaScript. Developers had to choose between:

  • Native scroll event handlers — synchronous listeners that cause main-thread jank, poor performance on low-end devices, and require manual throttling/debouncing.
  • IntersectionObserver — async, no exact progress tracking, only binary in/out state.
  • GSAP ScrollTrigger — powerful but adds ~60KB of JavaScript payload, licensing considerations, and main-thread execution.
  • requestAnimationFrame loops — manual progress tracking, difficult to optimize, memory-leaking if not cleaned up.

The CSS Scroll-Driven Animations specification, shipped in Chrome 115 (July 2023), introduces two new CSS properties:

PropertyPurposeExample Value
animation-timelineSpecifies what drives the animation's progressscroll(), view(), --my-timeline
animation-rangeConstrains the active range within the timelineentry, cover 20% 80%
scroll-timeline-nameNames a scroll container as a reusable timeline--my-scroll
scroll-timeline-axisDeclares the axis for a named scroll timelineblock, inline
view-timeline-nameNames a subject element as a view timeline--card-reveal
view-timeline-axisDeclares the axis for a named view timelineblock
Key Insight: Setting animation-duration: auto or omitting it entirely is required when using scroll-driven timelines. The browser automatically maps scroll progress 0–100% to animation progress 0%–100%.

The specification is a collaboration between Google, Apple, Mozilla, and Microsoft engineers within the CSS Working Group. The canonical reference is the CSS Animations Level 2 spec and the Scroll-driven Animations spec at the W3C.

2 scroll() vs view() Timelines — Choosing the Right Tool

The two timeline functions serve fundamentally different use cases. Understanding which to use is the most important conceptual step in mastering scroll-driven animations.

Featurescroll()view()
TracksScroll container's absolute offsetElement's intersection with scrollport
0% whenScroll container is at its topmost positionElement's leading edge enters the scrollport
100% whenScroll container is at its bottommost positionElement's trailing edge exits the scrollport
Best forReading progress bars, parallax backgrounds, sticky header opacityReveal on scroll, card animations, staggered list items
Axis paramblock, inline, x, yblock, inline, x, y
Container paramnearest, root, self, customImplicit (nearest scroll ancestor)

scroll() Syntax

@keyframes readingProgress { from { width: 0%; } to { width: 100%; } } .progress-bar { animation: readingProgress linear; animation-timeline: scroll(root block); }

view() Syntax

@keyframes revealCard { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: none; } } .card { animation: revealCard linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; }
When using view(), the animation-fill-mode: both (or the shorthand both in the animation property) is critical. Without it, the element may snap back to its initial state after the animation ends, creating a jarring flash.

3 Mastering animation-range — Precision Control Over Intersection

The animation-range property is the most nuanced part of the view() timeline system. It controls the precise sub-range of the element's intersection journey during which the animation is active. Without it, a view() animation runs during the entire time the element is intersecting the scrollport — from first touch to final exit — which is often not what you want.

The full set of named range keywords maps to specific phases of the element's intersection journey:

KeywordStart PointEnd PointUse Case
coverLeading edge enters scrollportTrailing edge exits scrollportFull parallax overlay effects
containTrailing edge enters scrollportLeading edge exits scrollportAnimations only while fully visible
entryLeading edge enters scrollportTrailing edge enters scrollportReveal/fade-in as element enters
exitLeading edge exits scrollportTrailing edge exits scrollportFade-out as element leaves
entry-crossingLeading edge enters scrollportLeading edge reaches centerSubtle entry cross-fade
exit-crossingTrailing edge reaches centerTrailing edge exits scrollportSubtle exit cross-fade

Beyond named keywords, animation-range accepts a start and end value for ultra-precise control. These values can be expressed as percentages or length units relative to the range keyword:

/* Only animate during the first 50% of the entry phase */ animation-range: entry 0% entry 50%; /* Start at 20% through entry, finish at 80% through exit */ animation-range: entry 20% exit 80%; /* Using the longhand properties */ animation-range-start: entry 0%; animation-range-end: entry 100%;
Pro Tip — Staggered Reveals: To create staggered list item reveals without JavaScript, use animation-range: entry 0% cover 30% with a custom property --i for each list item index, then compute animation-delay using calc(var(--i) * 50ms). This creates a beautiful waterfall effect with zero JS.

4 GPU Compositor Thread — Why Native CSS Beats JavaScript for Performance

The single most compelling reason to use native CSS Scroll-Driven Animations over JavaScript solutions is architectural performance superiority. To understand why, you need to understand how browsers process and render frames.

The Browser Rendering Pipeline

Every frame rendered by the browser goes through a pipeline: JavaScript → Style → Layout → Paint → Composite. The first four stages execute on the Main Thread — the same thread that handles JavaScript execution, HTML parsing, and CSS recalculation. If any stage takes too long, the frame is dropped and the user experiences jank.

The Composite stage, however, can execute on the GPU's Compositor Thread, which is completely independent of the Main Thread. Only two CSS properties can be composited without triggering Layout or Paint: transform and opacity.

ApproachThreadJank RiskMax FPSPayload
CSS SDA (transform/opacity)GPU CompositorNone120+0 KB JS
CSS SDA (layout props)Main ThreadMedium600 KB JS
GSAP ScrollTriggerMain ThreadMedium60~67 KB
scroll event + rAFMain ThreadHigh60Custom
IntersectionObserver onlyMain Thread (callback)Low-Med60Custom
Compositor-Only Properties: To guarantee your scroll-driven animation runs on the compositor thread and achieves maximum smoothness, only animate transform and opacity. Animating width, height, top, left, background-color, or border-radius will re-trigger Layout or Paint on the Main Thread, negating the performance advantage.

When a CSS scroll-driven animation only uses transform and opacity, Chrome's renderer creates a compositor animation. This means the browser can scroll the page and update the animation simultaneously, even if a 50ms JavaScript long task is blocking the main thread. The result is buttery-smooth 120fps animations on ProMotion displays, completely immune to main-thread congestion.

5 Named Scroll & View Timelines — Cross-Element Animation

Anonymous scroll() and view() timelines work when the animated element is the same as the scroll source or subject. But real-world designs often require animating one element based on the scroll progress of another — for example, animating a sticky sidebar based on the content section scroll, or animating multiple elements based on one shared scroll container.

This is solved by named timelines. You declare a scroll container or subject element as a named timeline using scroll-timeline-name or view-timeline-name, then reference that name from any descendant element's animation-timeline.

Named Scroll Timeline

/* Declare the scroll container as a named timeline */ .scroll-container { overflow-y: scroll; scroll-timeline-name: --section-progress; scroll-timeline-axis: block; } /* Animate a completely different element using that timeline */ .sidebar-indicator { animation: fillBar linear both; animation-timeline: --section-progress; }

Named View Timeline

/* Declare the subject element (the one being observed) */ .hero-image { view-timeline-name: --hero-view; view-timeline-axis: block; } /* Animate a child caption based on the parent image's intersection */ .hero-caption { animation: fadeInCaption linear both; animation-timeline: --hero-view; animation-range: entry 50% contain 0%; }
Scope: Named timelines are scoped to the element's subtree. A child element can reference a named timeline from any ancestor. However, a sibling cannot reference a sibling's timeline unless they share a common ancestor that re-exports it. This is intentional for encapsulation.

6 Easing Curves — Shaping the Animation Response Curve

The animation-timing-function (or the easing value in the animation shorthand) controls the rate of change of the animation progress. In time-based animations, this shapes how fast the animation accelerates and decelerates. In scroll-driven animations, it shapes how the animated property responds within each keyframe interval as scroll progress changes.

A crucial conceptual point: the overall animation-timeline progress is always linear with scroll position (scrolling down by 10% always advances the timeline by 10%). The easing function only applies within each keyframe interval, controlling how property values interpolate between keyframe stops.

Easing FunctionCurve ShapeBest Use Case in SDAFeel
linearStraight diagonalProgress bars, parallax, color gradientsMechanical, precise
easeSlow→fast→slowGeneral card revealsNatural, organic
ease-inSlow startExit animations (fading out)Gradual buildup
ease-outSlow endEntry animations (landing softly)Deceleration, landing
ease-in-outSlow both endsContained animations (fully visible)Polished, premium
cubic-bezier(0.68,-0.55,0.265,1.55)Overshoot + bouncePlayful icon revealsSpring, bouncy
steps(N, end)Stepped jumpsFrame-by-frame sprite animationCinematic, retro
linear(0,0.2,0.8 50%,1)Custom piecewiseComplex spring physics approximationEngineering precision
CSS linear() Function (Chrome 113+): The new linear() timing function accepts a list of points and optionally their positions, allowing you to encode complex spring/bounce physics as a pure CSS easing curve — something previously impossible. Use it to approximate GSAP Elastic or Bounce eases without any JavaScript.

7 Real-World Usage Patterns & Recipes

Beyond the basics, scroll-driven animations unlock a wide range of production-grade UI patterns. Here are the most commonly needed recipes with production-ready code.

Pattern 1 — Reading Progress Bar

.reading-bar { position: fixed; top: 0; left: 0; height: 4px; width: 0%; background: linear-gradient(90deg, #6366f1, #a855f7); animation: grow-bar linear; animation-timeline: scroll(root); } @keyframes grow-bar { from { width: 0%; } to { width: 100%; } }

Pattern 2 — Sticky Header Opacity Fade

header { position: sticky; top: 0; animation: header-blur linear both; animation-timeline: scroll(root block); animation-range: 0px 200px; } @keyframes header-blur { from { backdrop-filter: blur(0px); background: transparent; } to { backdrop-filter: blur(20px); background: rgba(0,0,0,0.8); } }

Pattern 3 — Staggered Card Reveal

.card { animation: card-reveal linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; } /* Stagger using :nth-child and custom property */ .card:nth-child(1) { --stagger: 0; } .card:nth-child(2) { --stagger: 50; } .card:nth-child(3) { --stagger: 100; } .card:nth-child(4) { --stagger: 150; } @keyframes card-reveal { from { opacity: 0; transform: translateY(40px) scale(0.95); } to { opacity: 1; transform: none; } }

Pattern 4 — Parallax Hero Image

.hero-image { animation: parallax-shift linear both; animation-timeline: view(); animation-range: cover 0% cover 100%; } @keyframes parallax-shift { from { transform: translateY(-20%); } to { transform: translateY(20%); } }

8 Browser Support, Polyfills & Progressive Enhancement Strategy

As of mid-2026, animation-timeline is natively supported in Chromium-based browsers representing approximately 65–70% of global web traffic. Firefox and Safari are in various stages of implementation. For production use today, a layered progressive enhancement strategy is recommended.

Layer 1 — CSS @supports Feature Detection

The most resilient approach is to use @supports to apply scroll animations only where natively supported, with a static fallback for other browsers:

/* Static state as base (works everywhere) */ .card { opacity: 1; transform: none; } /* Scroll enhancement for supporting browsers */ @supports (animation-timeline: scroll()) { .card { animation: reveal linear both; animation-timeline: view(); animation-range: entry; } }

Layer 2 — Google Chrome Labs Polyfill

For teams that need scroll animations to work identically across all browsers today, the official polyfill from Google Chrome Labs provides complete API coverage using IntersectionObserver and requestAnimationFrame as the fallback engine:

<script src="https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js" ></script>
Polyfill Performance Note: The polyfill runs on the Main Thread, so it does not inherit the GPU compositor performance advantage of native implementations. It is functionally equivalent but may exhibit slight jank on heavy pages with many animated elements. Native support is always preferable.

Layer 3 — IntersectionObserver for Older Browsers

For browsers that support neither native SDA nor the polyfill well (very old Firefox versions, IE 11), IntersectionObserver with a simple class toggle provides a usable fallback with CSS transitions:

// Simple IO reveal fallback const observer = new IntersectionObserver( (entries) => entries.forEach(e => e.target.classList.toggle('is-visible', e.isIntersecting) ), { threshold: 0.2 } ); document.querySelectorAll('.card').forEach(el => observer.observe(el));

9 CSS Scroll-Driven Animations vs GSAP ScrollTrigger — Detailed Comparison

GSAP ScrollTrigger has been the industry standard for scroll-linked animations since 2020. With native CSS SDA now shipping, many teams are evaluating whether they can replace GSAP. The answer depends heavily on use-case complexity.

CriteriaCSS Scroll-Driven AnimationsGSAP ScrollTrigger
JavaScript required✓ None✗ Required
Bundle size impact✓ 0 KB~67 KB (GSAP + ST)
Compositor thread✓ (transform/opacity)✗ Main thread
Browser support (2026)~70% native, polyfill for rest✓ All browsers
Scroll-linked pinning✗ Limited✓ Full support
Horizontal scroll sections⚠ Complex✓ Built-in
Callbacks / JS hooks✗ CSS-only✓ onEnter, onLeave, etc.
Ease authoringcubic-bezier, linear()Hundreds of named easings
DevTools integration✓ Chrome Animations panel⚠ Limited
Cost✓ Free (web standard)Free (non-commercial) / $150+ commercial
Recommendation: Use CSS Scroll-Driven Animations for entry/exit reveals, reading progress bars, parallax effects, and sticky header transitions. Use GSAP ScrollTrigger for complex pinning effects, horizontal scroll sections, sequenced multi-element timelines, and scenarios requiring JavaScript callbacks.

10 Accessibility — Respecting prefers-reduced-motion

Scroll-driven animations can cause significant discomfort for users with vestibular disorders, motion sensitivity, or epilepsy. The W3C recommends that any motion animation be suppressible by the user's OS-level Reduce Motion setting, exposed in CSS via the prefers-reduced-motion media query.

This is not optional for WCAG 2.1 AAA compliance and is strongly recommended for AA. All scroll-driven animations should respect this preference:

/* Default: animations enabled */ .card { animation: reveal linear both; animation-timeline: view(); animation-range: entry; } /* Disable ALL animations for motion-sensitive users */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }

For scroll-driven animations, the most semantically correct approach is to remove the animation-timeline and ensure the element's final state (visible, no transform) is preserved:

@media (prefers-reduced-motion: reduce) { .card { animation: none; opacity: 1; transform: none; } }
WCAG 2.1 — Success Criterion 2.3.3 (Animation from Interactions): "Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed." Scroll-driven animations triggered by scrolling fall under this criterion at AAA level. Always provide a no-motion alternative.

11 Performance Impact on Core Web Vitals & SEO

CSS Scroll-Driven Animations have a direct, measurable impact on Core Web Vitals — Google's primary performance signals used in ranking. Understanding this impact allows you to use the API to improve scores rather than hurt them.

Core Web VitalImpact of CSS SDACompared to JS Scroll Animations
LCP (Largest Contentful Paint)Neutral — no render-blockingBetter — no parser-blocking scripts
INP (Interaction to Next Paint)✓ Positive — compositor threadBetter — JS scroll listeners inflate INP
CLS (Cumulative Layout Shift)⚠ Risk if animating layout propsSame risk applies to JS animations
FID / TBT (Total Blocking Time)✓ Positive — zero JS parsingBetter — no JS runtime overhead
CLS Warning: Animating properties that affect layout (width, height, margin, padding, top, left) with scroll-driven animations will trigger Layout recalculation and may cause Cumulative Layout Shift, directly harming your CLS score. Always animate only transform and opacity to avoid CLS and maximize compositor performance.

From a pure SEO perspective, CSS SDA can improve your search ranking by:

  • Reducing JavaScript payload — eliminating scroll animation libraries reduces parse/execute time, improving TBT and indirectly LCP.
  • Improving INP — scroll listeners and rAF callbacks add microtask queue pressure. Replacing them with CSS SDA eliminates this entirely for supported browsers.
  • Enhancing engagement metrics — smooth scroll animations improve time-on-page and reduce bounce rate, both behavioral signals Google measures.
  • Improving perceived performance — progressive reveal animations make pages feel faster even if technical metrics are similar.

12 Debugging CSS Scroll-Driven Animations in Chrome DevTools

Chrome DevTools (115+) includes a dedicated Animations panel with full support for visualizing and scrubbing scroll-driven animations — a massive advantage over JavaScript-based animation debugging.

Opening the Animations Panel

  1. Open Chrome DevTools (F12 or Cmd+Opt+I)
  2. Click the three-dot menuMore toolsAnimations
  3. Or press Ctrl+Shift+P → type "Animations" → select "Show Animations"

Key DevTools Features for SDA

FeatureHow to Use
Scroll Timeline ScrubberIn the Animations panel, scroll-driven animations show a scrubber. Drag it to manually advance the animation timeline without scrolling.
Animation Delay/Duration EditClick any animation bar to edit its duration or delay inline — changes reflect live in the canvas.
Easing Curve EditorClick the cubic-bezier icon on any animation to open the graphical curve editor — adjust handles and see the animation update in real-time.
Compositor Layer InspectionIn Layers panel, verify your animated element is on its own compositor layer (shows as a separate green/blue rectangle).
Performance TimelineRecord a performance trace while scrolling. Look for "Composite" tasks on the GPU Process thread — scroll-driven CSS animations should appear there, NOT in the main thread flame chart.
Layer Promotion Check: Open DevTools → More Tools → Layers. If your scroll-animated element is NOT shown as a separate layer, it may not be GPU-composited. Force layer promotion with will-change: transform (use sparingly — it consumes GPU memory). Check "Why this layer was composited?" in the sidebar for the exact reason.

Common Debugging Scenarios

SymptomLikely CauseFix
Animation doesn't startanimation-fill-mode missing / wrong scroll containerAdd both to animation shorthand; check overflow on ancestors
Animation snaps at endMissing fill-mode: bothUse animation: name linear both
Animation jank on scrollAnimating layout-triggering propertiesStick to transform and opacity only
Works in Chrome, not SafariSafari partial supportAdd polyfill script or use @supports with static fallback
Animation-range not workingUsing scroll() instead of view()animation-range only applies to view() timelines

13 Advanced Techniques, Patterns & Upcoming CSS Features

Beyond the core primitives, CSS Scroll-Driven Animations unlock a set of powerful advanced patterns that are already used in production by companies like Apple, Google, and Stripe. Here we cover the cutting-edge techniques — including features landing in browsers in 2025–2026 — that will define the next generation of scroll-driven UI.

Pattern 5 — Pure CSS Scroll-Snapped Carousel

Combining scroll-snap-type with a scroll() timeline enables a snapped slide carousel with a native dot indicator that updates position without any JavaScript:

.carousel { display: flex; overflow-x: scroll; scroll-snap-type: x mandatory; scroll-timeline-name: --carousel; } .slide { scroll-snap-align: start; min-width: 100%; } .dot-active-indicator { animation: slide-dot linear both; animation-timeline: --carousel; }

Pattern 6 — Scroll-Linked Color Theme Transition

Animate CSS custom properties to smoothly transition entire page themes (background, text, accent colors) as the user scrolls through distinct content sections — a technique that was previously JavaScript-only:

/* Note: animating custom properties requires @property registration */ @property --bg-hue { syntax: '<number>'; initial-value: 220; inherits: true; } :root { animation: theme-shift linear both; animation-timeline: scroll(root block); } @keyframes theme-shift { from { --bg-hue: 220; } /* indigo at top */ to { --bg-hue: 160; } /* emerald at bottom */ }

Pattern 7 — Multi-Stage Animation with Keyframe Stops

You're not limited to from and to. Use percentage-stop keyframes to create multi-stage scroll animations — for example, an element that fades in as it enters, holds visible in the center, then fades out as it exits:

@keyframes enterHoldExit { 0% { opacity: 0; transform: translateY(30px); } 20% { opacity: 1; transform: none; } 80% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateY(-30px); } } .element { animation: enterHoldExit linear both; animation-timeline: view(); animation-range: cover; }

Upcoming: Scroll-Driven Animations in CSS Nesting & Layers

Two upcoming CSS features will make scroll-driven animations even more composable:

  • CSS Nesting (Chrome 112+): Combine @keyframes declarations directly inside rule blocks with native CSS nesting, eliminating the need to hoist keyframe declarations to the global scope.
  • CSS @layer: Scope your scroll animation rules inside a cascade layer (@layer animations { }) for better specificity management in design systems — crucial when adding scroll animations to component libraries.
  • view-timeline-inset: An upcoming property that adjusts the scrollport boundaries for view() timelines, similar to IntersectionObserver's rootMargin. Use it to trigger animations earlier or later than the natural viewport boundary: view-timeline-inset: 100px will trigger the animation 100px before the element enters the natural viewport.
  • CSS Scroll State Queries (Level 5): A proposed addition that would allow container queries to respond to scroll state — e.g., whether a sticky element is currently stuck. This would enable CSS-native sticky header style changes without JavaScript.

AEO & GEO Optimization: Direct Answer for AI Search

CSS Scroll-Driven Animations are best used for: reading progress bars (use scroll(root block) with a width keyframe on a fixed element), reveal-on-scroll card grids (use view() with animation-range: entry on each grid item), parallax backgrounds (use view() with animation-range: cover and opposing translate values), and sticky header blur transitions (use scroll(root block) with animation-range: 0px 200px and backdrop-filter keyframes). All these patterns require zero JavaScript, run on the GPU compositor thread, and produce no Cumulative Layout Shift when restricted to transform and opacity properties.

Production Checklist: Before shipping scroll-driven animations, verify: (1) animate only transform/opacity for compositor performance; (2) animation-fill-mode: both is set; (3) prefers-reduced-motion override is present; (4) @supports fallback or polyfill is configured; (5) test in Chrome Animations DevTools panel; (6) validate with Chrome Lighthouse to confirm no CLS regression.

FAQ Frequently Asked Questions

What are CSS Scroll-Driven Animations and how do they work?
CSS Scroll-Driven Animations (SDA) are a native web standard that allows you to link any CSS @keyframes animation directly to a scroll position — either the absolute scroll offset of a container (scroll() timeline) or the intersection progress of a specific element with the viewport (view() timeline). Instead of elapsing in real-time, animation progress is computed from scroll coordinates. When you scroll down by 10%, the animation advances by 10%. This all happens via two CSS properties: animation-timeline (which links the animation to a scroll or view timeline) and animation-range (which constrains the active sub-range of the intersection journey). Unlike JavaScript-based scroll animations, native CSS SDA runs on the GPU Compositor Thread — completely decoupled from the JavaScript Main Thread — achieving 120fps animations immune to main-thread jank.
What is the difference between scroll() and view() timeline functions?
scroll() and view() serve fundamentally different purposes. scroll() tracks the absolute scroll progress of a scroll container: 0% when the container is at the top, 100% when it reaches the bottom. It is ideal for reading progress bars, rotating elements as the page scrolls, sticky header transitions, and parallax background effects. view() tracks the intersection progress of a specific element with its nearest scroll ancestor (the 'scrollport'): 0% when the element's leading edge enters the viewport, 100% when its trailing edge exits. It is ideal for reveal-on-scroll effects, card fade-ins, image parallax that applies to individual elements, and staggered list animations. You can combine both in the same page — different elements can use different timeline types simultaneously.
What is animation-range and which values can it take?
animation-range is a CSS property that constrains the active portion of a view() timeline during which the animation plays. Without it, a view() animation runs during the entire intersection journey from first touch to final exit. The six named range keywords are: cover (entire intersection from leading edge enters to trailing edge exits — full duration parallax), contain (only while the element is fully inside the viewport), entry (only during the entering phase — ideal for reveal effects), exit (only during the exiting phase — ideal for fade-out effects), entry-crossing (element entering edge crosses the scrollport boundary), and exit-crossing (element exiting edge crosses the boundary). You can also combine keywords with percentages for ultra-precise control: animation-range: entry 0% entry 50% runs the animation only during the first half of the entry phase. The longhand properties animation-range-start and animation-range-end are also available.
Do CSS Scroll-Driven Animations hurt page performance or Core Web Vitals?
No — CSS Scroll-Driven Animations improve performance compared to JavaScript alternatives. When you animate only transform and opacity, the animation runs entirely on the GPU Compositor Thread, completely separate from the JavaScript Main Thread. This means even if a long JavaScript task is blocking the main thread, your scroll animations continue at 60–120fps with zero jank. Impact on Core Web Vitals: INP (Interaction to Next Paint) improves because you remove scroll event listeners from the main thread; LCP (Largest Contentful Paint) is neutral (no render-blocking); TBT (Total Blocking Time) improves because you eliminate JavaScript animation library parse/execute time (GSAP adds ~67KB). The one risk is CLS (Cumulative Layout Shift) — if you animate layout-triggering properties like width, height, or margin, you can cause CLS. Always stick to transform and opacity to avoid this.
Which browsers support CSS Scroll-Driven Animations natively in 2025–2026?
As of mid-2026, native support covers approximately 70% of global web traffic: Chrome 115+ (full support), Edge 115+ (full support), Chrome for Android 115+ (full support), Samsung Internet 23+ (full support for scroll()/view(), partial for animation-range). Firefox has partial support behind a flag in nightly builds. Safari 18+ has partial support. For production deployment targeting all browsers, use the official Google Chrome Labs polyfill (@bramus/scroll-driven-animations-polyfill) which uses IntersectionObserver and requestAnimationFrame as a fallback engine. Pair it with CSS @supports feature detection for a layered progressive enhancement strategy: define a visible static state as the base, then add scroll animations inside @supports (animation-timeline: scroll()) { }.
How do I use named scroll timelines to animate one element based on another element's scroll?
Named timelines solve the case where you need to animate one element based on the scroll progress of a different element. For scroll containers: add scroll-timeline-name: --my-timeline and scroll-timeline-axis: block to the overflow scroll container, then reference it with animation-timeline: --my-timeline on any descendant element. For view subjects: add view-timeline-name: --my-view and view-timeline-axis: block to the observed element (the one you want to track), then reference animation-timeline: --my-view from any related element (such as a caption below the image). Named timelines are scoped to the element's CSS subtree — a child can reference any ancestor's named timeline, but siblings cannot cross-reference each other's timelines directly. The shorthand scroll-timeline: --name axis combines both properties.
How should I handle CSS Scroll-Driven Animations for users with prefers-reduced-motion?
Scroll-driven animations can cause discomfort, dizziness, or seizures for users with vestibular disorders or motion sensitivity. WCAG 2.1 Success Criterion 2.3.3 (Animation from Interactions) at AAA level and WCAG 2.2 strongly recommend allowing users to disable motion triggered by interaction. The correct approach is to disable the animation-timeline and ensure the element's final state (visible, no transform) is shown to reduced-motion users. Use: @media (prefers-reduced-motion: reduce) { .animated-element { animation: none; opacity: 1; transform: none; } }. This preserves content visibility while removing motion. Do NOT simply reduce animation duration to 0.01ms for scroll-driven animations, as this can cause the element to snap invisibly. Always test with the OS-level Reduce Motion setting (macOS: Accessibility > Display > Reduce Motion; Windows: Settings > Ease of Access > Display > Show animations).
Can I use CSS Scroll-Driven Animations in production today without a polyfill?
Yes — with a progressive enhancement strategy. Use CSS @supports to apply scroll animations only where natively supported, while providing a static fallback for other browsers. The pattern is: set the element's final visible state as the base style (e.g., opacity: 1; transform: none), then inside @supports (animation-timeline: scroll()) { } or @supports (animation-timeline: view()) { }, apply the full scroll-driven animation including the from-state (opacity: 0, transform offsets). This way, on unsupported browsers the content is immediately visible and accessible, while on Chrome/Edge users get the smooth scroll animation. For teams needing pixel-identical cross-browser behavior today, add the @bramus/scroll-driven-animations-polyfill via CDN or npm, which parses your CSS and installs IntersectionObserver + requestAnimationFrame fallbacks transparently.
What is the difference between CSS Scroll-Driven Animations and GSAP ScrollTrigger?
CSS Scroll-Driven Animations and GSAP ScrollTrigger solve the same problem but with different tradeoffs. CSS SDA requires zero JavaScript, adds 0KB to your bundle, runs on the GPU compositor thread for maximum performance, is a free web standard, and integrates natively with Chrome DevTools' Animations scrubber panel. GSAP ScrollTrigger requires JavaScript (~67KB bundle), runs on the main thread, supports all browsers today, and provides advanced features like scroll-linked element pinning, horizontal scroll sections, timeline sequencing with callbacks (onEnter, onLeave, onUpdate), and hundreds of named easing functions. Recommendation: use CSS SDA for entry/exit reveals, reading progress bars, parallax effects, and sticky header transitions. Use GSAP ScrollTrigger for complex pinned scrolling, full-page horizontal sections, multi-element orchestrated sequences, and scenarios that require JavaScript event hooks.
How do I create a staggered reveal animation for multiple elements without JavaScript?
You can create a pure CSS staggered reveal using view() timelines and animation-delay with CSS custom properties. First, define your @keyframes and apply animation-timeline: view() with animation-range: entry to each element. Then use :nth-child() selectors to assign a different --stagger custom property value to each element, and compute animation-delay: calc(var(--stagger, 0) * 50ms). Since scroll-driven animations are linear with scroll position, the delay creates a beautiful waterfall effect where each card starts animating slightly later than the previous one. For a grid of 8 items, this means 0ms, 50ms, 100ms, 150ms, 200ms, 250ms, 300ms, 350ms delays — all computed in CSS. Alternatively, set animation-range: entry 0% entry calc(100% + var(--i) * 10%) to create range-based staggering where each item occupies a slightly different range window.
How do I debug CSS Scroll-Driven Animations in Chrome DevTools?
Chrome DevTools (115+) has native support for visualizing and scrubbing scroll-driven animations. Open DevTools (F12) → three-dot menu → More tools → Animations. Scroll-driven animations appear in the Animations panel with a specialized scrubber that you can drag to manually advance the animation without physically scrolling — invaluable for debugging animations at specific scroll positions. The easing curve editor allows graphical cubic-bezier adjustment with live preview. In the Layers panel (DevTools → More tools → Layers), verify your animated element is on its own compositor layer (shown as a distinct rectangle) confirming GPU compositing. In the Performance panel, record a scroll trace and check that Composite tasks appear on the GPU Process thread, not in the main thread flame chart. Common issues: animation not starting (missing animation-fill-mode: both), snapping at end (same fix), jank (animating layout properties — switch to transform/opacity only), not working in Safari (add polyfill or @supports fallback).
What is the CSS linear() easing function and how does it help with scroll animations?
The CSS linear() timing function (Chrome 113+, Firefox 112+) accepts a list of output values and optionally their positions, creating a piecewise linear easing curve that can approximate complex spring physics, bounce effects, and custom motion curves — something previously only possible with JavaScript. Unlike cubic-bezier() which is limited to a single smooth S-curve, linear() can encode any arbitrary curve shape by specifying enough sample points. For scroll-driven animations, this is particularly powerful because you can encode a spring overshoot or elastic bounce as pure CSS: animation-timing-function: linear(0, 0.04, 0.18, 0.44, 0.76, 1.04, 1.18, 1.2, 1.12, 1, 0.96, 0.98, 1). Tools like easings.dev and the linear-easing-generator on GitHub let you visually author these curves and copy the CSS output directly.

Rate CSS Scroll-Driven Animations Playground

Help us improve by rating this tool.

4.7/5
908 reviews