# CSS Scroll Driven Animations Without JavaScript

Source: https://www.egnworks.com/blog/css-scroll-driven-animations  
Author: Jacob Val  
Published: 2026-09-15  
Updated: 2026-09-15  
Category: Frontend Architecture  
Tags: Scroll Driven Animations, CSS

> A deep technical guide to CSS scroll-driven animations, covering animation-timeline, the scroll() and view() functions, named scroll and view timelines, animation-range, and how to build scroll progress bars and reveal effects without a scroll event listener.

---

A progress bar that fills as a visitor scrolls, an image that fades in as it enters the viewport, a header that shrinks as the page moves past it, all of these have traditionally required a scroll event listener, a read of `window.scrollY` or an `IntersectionObserver`, and a manually written function that maps scroll position to a style change on every single frame. CSS scroll driven animations remove the JavaScript entirely by letting an animation's timeline be scroll position itself rather than time.

## Why This Used to Require JavaScript

A standard CSS animation runs against the document timeline, which advances at a fixed rate regardless of anything the user does. There has never been a CSS native way to say an animation should progress in step with how far a container has scrolled, or with how far an element has moved through the viewport, so any effect tied to scroll position had to be built by hand.

The typical implementation attaches a `scroll` listener, reads the current scroll offset on every event, computes a progress value between 0 and 1, and writes that value into a CSS custom property or directly into an inline style. Because scroll events fire far more often than the browser can usefully repaint, this usually gets wrapped in `requestAnimationFrame` to avoid doing the calculation more than once per frame. It works, but it is a nontrivial amount of code for something that is fundamentally a mapping from one number to another.

## The Two Kinds of Scroll Driven Timeline

`animation-timeline` replaces the default document timeline with one of two scroll based alternatives. A scroll timeline tracks the scroll position of a scrollable container. A view timeline tracks how far a specific element has moved through that container's visible area.

```css
.progress-bar {
  animation-timeline: scroll(nearest block);
}

.reveal-on-scroll {
  animation-timeline: view();
}
```

`scroll()` takes an optional scroller keyword, `nearest`, `root`, or `self`, and an optional axis, `block`, `inline`, `x`, or `y`. `view()` takes only an axis, since the element being animated is always the one whose position in the viewport is being tracked. Both attach to whichever `animation` is already declared on the element, so the keyframes themselves look exactly like a normal CSS animation.

## Building a Scroll Progress Bar

A progress bar that fills as the page scrolls needs a scroll timeline on the page itself and an animation on the bar that goes from empty to full.

```css
@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  width: 100%;
  transform-origin: left;
  background: royalblue;
  animation: grow-progress linear;
  animation-timeline: scroll(root block);
}
```

`scroll(root block)` ties the animation to the vertical scroll position of the document itself. As the page scrolls from top to bottom, the animation progresses from its `from` keyframe to its `to` keyframe, so the bar grows in lockstep with scroll position. No JavaScript computes the percentage, and no listener runs on every scroll event.

## Naming a Timeline for Reuse

`scroll()` and `view()` attach a timeline to the element they are declared on. A named timeline, declared with `scroll-timeline-name` or `view-timeline-name`, lets one element define the timeline while a different element's animation reads from it.

```css
.gallery {
  view-timeline-name: --gallery-in-view;
}

.gallery-caption {
  animation: fade-in linear;
  animation-timeline: --gallery-in-view;
}
```

This separates the element whose position defines progress from the element whose style actually changes, which matters when the visual effect belongs on a child, a sibling, or an unrelated element entirely, rather than on the exact element scrolling into view.

## Controlling Where the Animation Starts and Ends

By default, a view timeline runs for the entire time an element is anywhere within the scrollport, from the moment it first appears at one edge to the moment it fully leaves the other. `animation-range` narrows that window to a specific portion of the timeline.

```css
.reveal-on-scroll {
  animation: fade-in-up linear;
  animation-timeline: view();
  animation-range: entry 10% cover 40%;
}
```

The range keywords, `entry`, `contain`, `cover`, and `exit`, describe named phases of an element's transit through the viewport, and each can take a percentage that offsets it within that phase. This is what makes a reveal-on-scroll effect finish well before an element reaches the center of the screen, rather than continuing to animate all the way until it is fully visible.

## What Still Needs JavaScript

Scroll driven animations only affect what CSS animations already affect, transform, opacity, color, and other animatable properties. Reading scroll position for anything outside styling, updating a piece of application state, triggering a network request, or firing an analytics event when a section becomes visible, still requires JavaScript, typically an `IntersectionObserver` for the visibility case rather than a scroll listener.

The two also compose well together. A component can use CSS for the purely visual scroll effect and JavaScript only for the small piece of logic that CSS cannot express, rather than reimplementing the whole animation by hand.

## Browser Support and a Fallback Path

Scroll driven animations currently run in Chromium based browsers, and MDN still lists `animation-timeline`, `scroll()`, `view()`, and `animation-range` as not yet Baseline, meaning support in Safari and Firefox should not be assumed without checking current tables first. Because the feature degrades by simply not animating rather than throwing an error, wrapping the relevant rules in an `@supports (animation-timeline: view())` block is enough to keep the element in its final, resting state on a browser that has not yet implemented the timeline functions.

```css
@supports (animation-timeline: view()) {
  .reveal-on-scroll {
    opacity: 0;
    animation: fade-in-up linear;
    animation-timeline: view();
    animation-range: entry 10% cover 40%;
  }
}
```

Declaring the starting `opacity: 0` only inside the `@supports` block avoids hiding the content permanently on a browser that never runs the animation.

## When to Reach for It

| Situation | Recommendation |
| --- | --- |
| A progress bar, parallax effect, or reveal-on-scroll animation | Use a scroll or view timeline, since the effect is purely visual and CSS already expresses it |
| A section that should trigger a side effect, such as loading more content, once visible | Use an `IntersectionObserver`, since scroll driven animations cannot run script |
| Support for a browser without the feature is required | Wrap the animation in `@supports` and keep the unanimated state usable on its own |

## Conclusion

CSS scroll driven animations move an entire category of effects, progress bars, parallax, and reveal-on-scroll transitions, out of a hand rolled scroll listener and into the same declarative model ordinary CSS animations already use. `animation-timeline` decides whether progress comes from a container's scroll offset or an element's transit through the viewport, `animation-range` narrows exactly which portion of that transit matters, and the browser handles the frame by frame work that used to be a `requestAnimationFrame` loop.

## References

[MDN: CSS Scroll-Driven Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations)

[MDN: animation-timeline](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline)

[MDN: scroll()](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline/scroll)

[MDN: view()](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline/view)

[MDN: animation-range](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-range)

[Can I Use: CSS Scroll-Driven Animations](https://caniuse.com/css-scroll-driven-animations)
