tool / scroll-driven cssview() · scroll() · animation-range

CSS scroll-driven animation generator

Pick an effect, choose the stretch of scroll it plays over with animation-range, and watch it run on animation-timeline: view() or scroll() in a box you scroll. Then copy the CSS, with a fallback for browsers that lack it, or the same animation as svg-scroll-draw code.

Presets
7
Ranges to pick
4
Easings
4
JS per frame
0
abcdviewport · Helement · h0hHH + hscrollcoverentrycontainexit
Fig. 1 — an element crossing its viewport, at the four moments that bound a view() timeline’s ranges: cover is a → d, entry a → b, contain b → c, exit c → d. svg-scroll-draw’s default trigger, ‘top bottom’ → ‘bottom top’, is a → d.
01The tool

Make a scroll-driven animation

Checking your browser for scroll timelines…

range start0%
range end50%

CSS mode: each card runs its own view() timeline on this box, its nearest scroller. The figures are each card’s progress as the browser reports it, through the same easing. The bars beside the box are where each card’s range falls on the box’s scroll; the line is where it is now.

styles.css20 lines
/* Fade up, tied to the scroll. Markup: <div class="reveal">…</div> */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: translateY(0px); }
}

/* Only where the browser has scroll timelines, and only for readers who have
   not asked for less motion; everywhere else the element is simply shown. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation-name: fade-up;
      animation-duration: auto;
      animation-timing-function: cubic-bezier(0.333333, 0.666667, 0.666667, 1); /* ease-out: t(2 − t) */
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 0% cover 50%;
    }
  }
}

svg-scroll-draw · runs this on its JS engine

Only the default window, cover 0% to cover 100% (trigger 'top bottom' to 'bottom top'), compiles to CSS. entry 0% cover 50% runs on scrollAnimate’s JS engine in every browser, over the same stretch of scroll.

This range, as written: animation-range: entry 0% cover 50%.

026 named ranges

How animation-range works

A view() timeline follows one element through its scroller: 0% when its top reaches the bottom of the view, 100% when its bottom leaves the top (fig. 1). animation-range picks the stretch of that journey the animation plays over, as a named range and a percentage at each end. entry 0% cover 50% starts as the element appears and finishes as its centre reaches the centre of the view. Before the start it holds its first keyframe and after the end its last, which is what animation-fill-mode: both does.

Range0% is when the element’s…100% is when…Scrollsvg-scroll-draw trigger
coverits top meets the bottom of the scrollerits bottom meets the topH + h'top bottom' → 'bottom top'
entryits top meets the bottomits bottom meets the bottom: fully inh'top bottom' → 'bottom bottom'
containits bottom meets the bottom: fully inits top meets the top: about to leaveH − h'bottom bottom' → 'top top'
exitits top meets the topits bottom meets the top: goneh'top top' → 'bottom top'

H is the scroller’s height and h the element’s, for an element shorter than its scroller. Cover: The whole crossing, from the first pixel in to the last pixel out. A view() timeline with no range runs over cover. Entry: While it comes in. The usual range for a reveal. Contain: While all of it is in view. For an element taller than the scroller it flips: while it covers the whole view. Exit: While it goes out. Pair it with a reversed animation to fade things away.

Taller elements
For an element taller than its scroller, contain flips: it runs while the element covers the whole view, from its top at the top to its bottom at the bottom. The other two names, entry-crossing and exit-crossing, differ from entry and exit only then: entry runs until the top reaches the top, entry-crossing until the bottom reaches the bottom.
Percentages
0% and 100% are the ends of the named range, but values past them (cover 120%) and lengths (entry 40px) work too, and the two ends can use different names: entry 0% exit 100% is the same stretch as cover.
03Two timelines

view() and scroll()

Both are anonymous timelines: they find their scroller themselves. scroll() is a scroller’s own position, 0% at the top and 100% at the bottom; scroll(root) is the page, and scroll() on its own the nearest scrolling ancestor. view() is one element’s way through its nearest scroller, which is why the same CSS works on a page and in the generator’s box. When the element that moves is not the one crossing the screen, name a timeline instead.

timelines.css26 lines
/* view(): this element’s own way through its nearest scroller. */
.card {
  animation: fade-up auto linear both;
  animation-timeline: view();          /* after the shorthand, which resets it */
  animation-range: entry 0% cover 50%;
}

/* scroll(): a scroller’s position, 0% at the top, 100% at the bottom.
   root is the page; nearest, the default, is the closest scrolling ancestor. */
.progress {
  animation: grow auto linear both;
  animation-timeline: scroll(root);
}

/* A named timeline, for when the element that moves is not the one that
   crosses the screen: the section names it, its children use it. */
.section { view-timeline: --section block; }
.section .title {
  animation: fade-up auto linear both;
  animation-timeline: --section;
  animation-range: entry 0% entry 100%;
}

/* timeline-scope lifts the name to an ancestor, so elements outside the
   section can use it too. */
.page { timeline-scope: --section; }
The shorthand
animation resets animation-timeline, so write the timeline after it, or use the longhands as the generator does.
overflow: hidden
It makes an element a scroll container. An ancestor with it becomes the scroller view() follows, and since that box never scrolls, nothing moves. overflow: clip clips without it.
04Detect, then decide

Browser support and the fallback

Test for the feature instead of the browser: @supports (animation-timeline: view()) in CSS, CSS.supports('animation-timeline: view()') in JavaScript, the same test svg-scroll-draw makes before its native path. Where it stands in November 2026:

BrowserSinceNotes
Chrome, Edge115Since July 2023: view(), scroll(), animation-range and named timelines. Opera, built on the same Chromium, has them too.
Safari (macOS, iOS)26Shipped with Safari 26. Earlier Safari has none of it.
Firefox159On by default since October 2026. Before that it sat behind a preference, off for most readers.

A browser without it skips the whole block, so the element shows as written: nothing hidden, nothing broken, just no motion. For most reveals that is the right fallback. When the motion matters, run the same animation in JavaScript only where the test fails: the generator’s fallback option writes that file, with svg-scroll-draw over the same stretch of scroll.

detect.css9 lines
/* The animation, only where the browser can run it. Everywhere else
   this block is skipped and the element shows as written. */
@supports (animation-timeline: view()) {
  .reveal {
    animation: fade-up auto linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 50%;
  }
}
detect.js3 lines
if (!CSS.supports('animation-timeline: view()')) {
  // No scroll timelines here: run the JavaScript version, or nothing.
}
Reduced motion
The generated CSS sits inside @media (prefers-reduced-motion: no-preference), so readers who ask for less motion get no animation and see the element as written: for an effect that plays in, its end state. svg-scroll-draw skips the animation too, also when the setting changes while the page is open, but sets the end values, so a fade that plays out leaves scrollAnimate’s element hidden; its clip reveal shows in full either way. The progress bar is left alone: it only moves as the reader scrolls.
05svg-scroll-draw

The same CSS, from the library

scrollAnimate and scrollDraw have a native path. When the browser passes the test and the options are ones CSS can express, they write a <style> with @keyframes and a view timeline (view() for scrollAnimate, one named on the container for scrollDraw), add a class, and leave the scroll to the browser, with no JavaScript per frame. Otherwise they run their own engine, a frame loop that reads the scroll while the element is on screen, with the same curve over the same stretch of scroll.

main.js5 lines
import { scrollAnimate } from 'svg-scroll-draw';

scrollAnimate('#card', {
  props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0px)'] },
});
what it writes13 lines
/* What it writes in a browser with view() (one line in a <style>; formatted here). */
@keyframes ssd-a-1 {
  from { opacity: 0; transform: translateY(40px) }
  to   { opacity: 1; transform: translateY(0px) }
}
.ssd-a-1 {
  animation-name: ssd-a-1;
  animation-duration: auto;
  animation-timing-function: cubic-bezier(0.333333, 0.666667, 0.666667, 1);
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
}

It takes that path only when all of these hold:

ConditionWhy
native is not falseIt is on by default; native: false always runs the JavaScript engine.
The browser supports view()Tested with CSS.supports(), as above. scrollDraw also needs named view timelines: it names one on the container you pass, so that box sets the range.
easing is linear, ease-in, ease-out, ease-in-outWritten as the curves the engine runs, not CSS’s keywords of the same name: ease-in and ease-out exactly, as t² and t(2 − t), and ease-in-out as linear() through 33 stops. The library writes no CSS for spring, bounce, elastic or a function, so those stay on the engine.
The default trigger, 'top bottom' → 'bottom top'That is cover 0% → cover 100%. Any other window, like the generator’s default, runs on the engine over the same stretch.
once off, speed 1, axis yCSS progress cannot latch at the furthest point reached, and the native path follows the vertical axis only.
No scrollContainer, no triggerElementThe CSS path measures the animated element (for scrollDraw, its container) in its nearest scroller, so a container you name, or scrollAnimate’s separate trigger element, keeps the engine.
No callbacks, no velocityScaleonProgress, onComplete, onEnter and the rest need a frame of JavaScript to fire from.
scrollAnimate: props CSS can animateopacity, transform, background-color, color, filter, scale, translate, rotate.
scrollDraw: plain drawingNone of stagger, clip, morphTo, waypoints, repeat, delay, autoReverse, or stroke and fill changes. From 2.23.3, no autoplay or debug either: in 2.23.2 and earlier, autoplay: true or debug: true on an otherwise plain drawing still took the CSS path, so an autoplay drawing followed the scroll instead of playing over its duration.
The preview
In svg-scroll-draw mode the generator passes its box as scrollContainer, which keeps the library on its engine: you are watching the fallback, the code a browser without scroll timelines runs. One difference shows on entry: the engine wakes an element when its box comes into view, and a y offset or a small scale holds that box back, so it starts later than the CSS and catches up in one step.
Which to use
When the CSS alone does the job, use the CSS. The library is for what it cannot do: older browsers, playing once and staying, callbacks, staggered groups, line drawings with measured lengths, split text.
067 questions

Scroll-driven animation questions

Q1

What are CSS scroll-driven animations?

Animations whose progress follows scrolling instead of a clock. You write ordinary @keyframes, set animation-timeline to view() (an element crossing its scroller) or scroll() (a scroller’s position), and animation-range to choose which stretch of the scroll plays them. The browser runs them with no JavaScript, and for transform and opacity it can keep them off the main thread.

Q2

What is the difference between view() and scroll()?

scroll() follows a scroller’s position, 0% at the top and 100% at the bottom, the same for every element that uses it: it suits progress bars and effects tied to the whole page. view() follows one element’s journey through its scroller, from its first pixel in to its last pixel out, so each element gets its own timeline: that is the one for reveals. Both find their scroller themselves; named timelines (scroll-timeline, view-timeline) point at a particular one.

Q3

How does animation-range work?

It sets where on the timeline the animation starts and ends. For view(), each end is a named range and a percentage: cover is the whole crossing, entry while the element comes in, exit while it goes out, contain while it is fully in view. entry 0% cover 50% starts as the element appears and finishes when its centre reaches the centre of the view. Without animation-range, a view() animation runs over all of cover.

Q4

Which browsers support animation-timeline?

Chrome and Edge since 115, Safari since 26, and Firefox since 159, which turned it on by default in October 2026; before that Firefox kept it behind a preference. Older browsers ignore it. Rather than trust a table, test it: @supports (animation-timeline: view()) in CSS, or CSS.supports('animation-timeline: view()') in JavaScript. The generator above runs that test on your browser.

Q5

Why is my scroll-driven animation not working?

The usual causes: animation-timeline comes before the animation shorthand, which resets it; an ancestor has overflow: hidden, which makes it a scroll container, so view() follows that box, which never scrolls (overflow: clip does not do this); a named range such as entry or exit is used with scroll(), whose timeline has none, so the browser ignores it and plays over the whole scroll; or the browser has no support and the @supports block is skipped. The generator’s reveal CSS is also off, on purpose, for readers who ask for reduced motion.

Q6

Can a CSS scroll-driven animation play only once?

No. Its progress is the scroll position, so scrolling back plays it backwards. To play once and stay, you need JavaScript: add a class with a time-based animation when the element enters, or use svg-scroll-draw’s once option, which keeps the furthest progress reached. once runs on the library’s JavaScript engine, since CSS cannot latch.

Q7

Does svg-scroll-draw use CSS scroll-driven animations?

Yes, when it can. scrollAnimate writes @keyframes with animation-timeline: view() and animation-range: cover 0% cover 100%, and scrollDraw the same on a view timeline named on its container, when the browser supports it and the options are ones CSS can express (the default trigger, an easing CSS can write, no once, no callbacks), and run their JavaScript engine everywhere else, to the same result. native: false turns the CSS path off.

The native path in depth: zero-JS SVG scroll animations with native CSS. All of svg-scroll-draw 2.23.3: the docs, or the text animation generator for words and letters.