Free tool - no signup
Scroll Timeline Playground
Pick the timeline that drives an animation, choose the stretch of it the animation plays over, and decide who can see a named timeline. The preview scrolls for real, the CSS under it is the CSS that ran, and a switch compares how the 2023 and 2026 drafts look a timeline name up. Everything runs in your browser and nothing is uploaded.
Reduced motion is on, so the animation is paused. The CSS below is unchanged.
The bar starts filling when the scroller is at its start, and it is full when the scroller is scrolled all the way to its end. In this demo that is 0% to 100% of the timeline.
Each bar follows the scroller in its own card.
timeline-scope on .card fences the name in, so each bar only matches a timeline inside its own card.
.card { timeline-scope: --panel; }
.scroller {
overflow-y: auto;
scroll-timeline: --panel block;
}
.bar { transform-origin: 0 50%; }
@supports (animation-timeline: scroll()) {
.bar {
transform: scaleX(0);
animation: fill auto linear both;
animation-timeline: --panel;
}
}
@keyframes fill { to { transform: scaleX(1); } } How to use it
- Scroll inside the box in the preview. The blue bar fills as the timeline advances, and the strip under it marks the part of the timeline the animation covers.
- Switch between scroll() and view(), then try the other axis. On view() the range names unlock, and the strip splits into entry, contain and exit.
- Turn timeline-scope off with a named timeline and flip the rule switch. Read the line under it that says which scroller each bar follows, then copy the CSS.
What a timeline replaces
A normal animation runs on the clock. A scroll-driven one runs on a position instead: the first frame is the top of the scroller, the last frame is the bottom, and scrolling backwards plays it backwards. The keyframes don't change. Only the thing that moves the playhead does.
That is why animation-duration becomes auto
in the printed CSS. There's no length of time to state when the length
is a scroll distance. The easing matters too, and it surprises people:
the default ease would bend the bar against the scroll, so
the tool prints linear, which is what tracks the scrollbar
one to one.
Choosing the range
By default the animation plays across the whole timeline. For a scroll timeline that is the scrollable distance, and you can trim it with percentages: start at 20% and end at 80% and the bar sits empty for the first fifth and full for the last. For a view timeline the useful handles are the named ranges.
cover runs from the element's leading edge touching the far
side of the scrollport to its trailing edge leaving the near side.
entry is the first part of that, while the element is
still arriving. exit is the last part, while it leaves.
contain is the stretch between them, where the element is
fully inside the scrollport, or, for an element larger than it, fully
covering it. Add a percentage and you move within that segment, so
entry 25% is a quarter of the way through the arrival.
The inset changes where those edges are measured. A positive inset shrinks the region that counts as visible on both sides, which makes the element enter later and leave sooner. The strip redraws when you change it, so you can watch the three segments slide.
Named or anonymous?
An anonymous scroll() or view() is the short
form: no names, no extra rules, and it only works when the animated
element can find its scroller by walking up the tree. The bar in the
demo can't, because it sits beside the scroller in the markup rather
than inside it. That is the case a named timeline exists for:
scroll-timeline: --panel block on the scroller,
animation-timeline: --panel on the bar.
Where timeline-scope comes in
A name has to be findable. Under the 2023 text it is visible only to
the element that declares it and that element's descendants, so
timeline-scope: --panel goes on a common ancestor to widen
the reach. Under the 2026 text names are global and the same property
fences them in. One line in one place, timeline-scope on
the nearest common ancestor, is correct under both. The moment you leave
it out and put two copies of a component on one page, the result is
defined by which text your browser follows. The
Safari Technology Preview write-up
covers the build that changed it and what the notes say shipped.
Reduced motion
With prefers-reduced-motion: reduce set, the preview pauses
its own animation and offers a button to run it anyway. A scroll-driven
effect is under the reader's hand, which makes it gentler than a timed
one, but a bar that sweeps while the whole page moves can still be too
much. The CSS the tool prints doesn't include a reduced-motion rule,
because what to replace the motion with depends on the effect. To see
a reduced version beside the original, the
reduced motion preview
runs both at once.
Common questions
- What is the difference between scroll() and view()?
- scroll() follows how far a scroller has travelled, from its start to its end. view() follows one element crossing a scroller, from the moment its leading edge appears to the moment its trailing edge leaves. A progress bar for a whole page wants scroll(). A card that fades in as it arrives wants view().
- Why are the range names greyed out on scroll()?
- Because cover, contain, entry and exit describe how an element sits inside a scrollport, and a scroll timeline has no element to measure. On scroll() the range takes plain percentages of the scrollable distance, and the tool resets any name you had chosen so the CSS it prints never carries a dead keyword.
- What does timeline-scope do?
- It decides who can find a named timeline. In the June 2023 Working Draft a name is visible to the element that declares it and to that element's descendants, so a bar sitting beside a scroller needs timeline-scope on a shared ancestor to reach it. In the September 2026 Editor's Draft names are global by default and timeline-scope fences a name in. Put it on the nearest common ancestor and the CSS means the same thing under both texts.
- Why do the bars behave differently with timeline-scope switched off?
- Because that is the one case where the two drafts disagree. Under the 2023 text neither bar is a descendant of a scroller, so neither can see the name and both stay put. Under the 2026 text both bars walk up to the document root and take the last matching timeline in tree order, which is the right-hand scroller. The preview shows what your browser does. The panel under it says what each text says, and the two only match if your browser has caught up. We loaded the page in Chrome 152 on 17 October 2026 with the switch off: neither bar moved when either box scrolled, which is the 2023 behaviour. The bars rested full rather than empty, because the printed animation has a fill mode and a name that matches nothing leaves the animation parked at its end state. The signal is that they don't move, not where they stop.
- Why is the animation wrapped in @supports?
- Because the starting state and the animation have to arrive together. The bar starts at scaleX(0) and the timeline is what fills it. A browser that doesn't know animation-timeline would get the empty start and nothing to advance it, so the bar would sit at zero forever. Inside @supports (animation-timeline: scroll()) that browser skips both and shows the bar in its normal, full state.
- Why does animation-timeline come after the animation shorthand?
- The animation shorthand resets animation-timeline to auto. Write it first and the shorthand wipes it out, and the animation quietly goes back to the clock. The tool prints them in the order that works.
- Does anything I change get uploaded?
- No. The model, the preview and the CSS all run in your browser tab, and there is no request, account or stored state.
The longer story of how timing works once scroll replaces the clock is in scroll-driven animation timing patterns. To change any declaration and watch it move, open the live CSS preview, and for the delay between items in a list, use the stagger calculator.