Anonymous vs Named Scroll Timelines: When Each Wins
animation-timeline accepts two fundamentally different kinds of value: anonymous timeline functions — scroll() and view() — resolved on the spot relative to the animated element, and named timelines declared elsewhere with scroll-timeline-name / view-timeline-name and referenced by <dashed-ident>. Both drive the same underlying progress timelines; what differs is coupling, reach, and how failures present when something moves in the DOM. This comparison — part of the Named Timelines & timeline-scope guide within Scroll-Driven & View Transition Implementation Patterns — gives you decision criteria, a refactoring path in each direction, and the cascade and debugging differences that decide real incidents.
When to use this approach
Choosing between an anonymous function and a name is a coupling decision, not a capability one:
- Anonymous
scroll()/view()wins for self-contained effects. A card that fades itself in withanimation-timeline: view(), a hero that parallaxes againstscroll(root)— declaration and consumption on the same element, zero coordination, copy-paste portable between projects. Which function to use is its own question, covered in scroll() vs view(): choosing the right timeline function. - Anonymous wins when “nearest scroller, whatever it is” is the actual requirement.
scroll(nearest)andview()rebind automatically if the component is dropped into a different scroll container. That looseness is a feature in design-system components shipped to unknown DOM. - Named wins the moment two elements must agree on one timeline. Several consumers sampling the same scroller at different ranges, or a consumer in a different branch than the scroller (via
timeline-scope), cannot be expressed with anonymous functions at all —scroll()can only see the element’s own ancestor chain, and always picks its nearest scroller, which may differ per consumer. - Named wins for explicitness at scale. A dashed-ident is greppable;
scroll(nearest)means “whichever ancestor happens to scroll today.” When a wrapper gainsoverflow: autoin a refactor, everyscroll(nearest)beneath it silently retargets; a named binding keeps pointing at the declared scroller or fails visibly. - Anonymous wins on minimum-version support.
scroll()/view()and named lookup both shipped with Chrome 115, but the cross-branchtimeline-scopehalf of the named story needs 116 — an extra@supportsprobe if you hoist.
Implementation
Refactoring between the two forms is mechanical once you know which sub-properties each side touches. Both directions below use a reading-progress bar as the worked example — the anonymous version matches the reading progress bar walkthrough.
Refactoring anonymous → named
Start state — consumer binds directly to the root scroller:
.progress {
transform-origin: 0 50%;
animation: grow linear both;
animation-timeline: scroll(root);
}
-
Declare the name on the scroller you actually mean.
scroll(root)means the document scroller, so name it there:html { scroll-timeline: --page-scroll y; }Match the axis to what the anonymous function implied:
scroll(root)defaulted to theblockaxis, soblock(oryin a fixed horizontal-writing context) preserves behavior. -
Swap the function for the ident — keeping the longhand after the
animationshorthand, since the shorthand resetsanimation-timeline:.progress { transform-origin: 0 50%; animation: grow linear both; animation-timeline: --page-scroll; } -
Add
timeline-scopeonly if a consumer is not a descendant of the scroller. Forhtmlas scroller it never is — every element descends fromhtml. For an inner pane feeding a sibling toolbar, hoist on the common ancestor; the full wiring is in sharing a scroll timeline across components. -
Migrate
view()the same way withview-timelineon the subject element (the one whose visibility is tracked). This is the one refactor that changes semantics meaningfully:animation-timeline: view()tracks the consumer itself, while a named view timeline can track a different element — you can now fade a caption as its image crosses the viewport.view-timeline-insetstays a separate longhand; it is not part of theview-timelineshorthand. -
Consolidate duplicates. After the swap, any siblings that carried their own identical
scroll(root)bindings can reference--page-scrollinstead — one greppable name, one place to retarget.
Refactoring named → anonymous
Going the other way is warranted when a component leaves shared-timeline duty and needs portability — e.g., extracting a self-fading card out of an app shell into a design system.
- Confirm the element is the only consumer of the name (grep the ident).
- Replace the ident with the equivalent function: nearest-ancestor scroller →
scroll(), document scroller →scroll(root), tracked own visibility →view(). There is no anonymous equivalent of “that specific scroller over there” — if the binding relied ontimeline-scopeto reach a non-ancestor, it cannot become anonymous. - Delete the now-unconsumed
scroll-timeline/view-timelinedeclaration and anytimeline-scopeentry that listed the name. Leaving them costs nothing at runtime but rots into misleading documentation.
The scroll timeline API reference covers the full argument grammar for both functions — scroll(nearest | root | self, block | inline | x | y) and view(<axis> <inset>) — if you need the less common variants.
Verification
The two forms fail differently, and your debugging workflow should exploit that.
Anonymous timelines fail by binding to the wrong thing. scroll(nearest) under a wrapper that unexpectedly gained scrollable overflow rebinds without complaint; the animation still runs, just against the wrong scroller. Verify which source is bound:
const anim = document.querySelector('.progress').getAnimations()[0];
// ScrollTimeline exposes its source element
console.log(anim.timeline.source); // expect: document.scrollingElement
console.log(anim.timeline.axis); // expect: "block" or "y"
Named timelines fail by going inactive. A typo’d ident, a missing scope, or duplicate declarers all produce the same symptom — the element sits at its base styles. Check resolution rather than motion:
const anim = document.querySelector('.progress').getAnimations()[0];
console.log(anim.timeline); // null / inactive ⇒ name never resolved
console.log(anim.timeline?.currentTime); // null ⇒ inactive timeline
Then in the Elements panel: Computed → animation-timeline shows the ident on the consumer, and the scroller must show a matching scroll-timeline-name. If the consumer’s computed value shows auto instead of your ident, a later animation shorthand reset it — the cascade gotcha below.
For both forms, finish with a Performance-panel scroll recording: progress ticks on the compositor track, no per-frame style recalc storms.
Edge cases and gotchas
The animation shorthand resets both forms equally. animation: grow linear both resets animation-timeline to auto whether you meant scroll(root) or --page-scroll. Every rule that sets the shorthand must re-state the timeline longhand after it. This is the single most common breakage in code review.
Neither form inherits — but idents cascade as names, functions do not. animation-timeline itself is not inherited in either form. The difference is upstream: a named timeline’s declaration (scroll-timeline-name) lives on the scroller and is visible to an entire subtree via lookup, so moving a consumer within that subtree preserves the binding. An anonymous function re-evaluates nearest at the consumer’s new position — same CSS, potentially different scroller.
Dashed-idents are not custom properties. --page-scroll cannot be set via var(), cannot be defined with @property, and does not inherit as a value. animation-timeline: var(--tl) where --tl: --page-scroll does work in Chromium as ordinary custom-property substitution — but it defeats greppability and confuses DevTools traces; avoid it.
Specificity conflicts resolve per-property, not per-strategy. If one rule sets animation-timeline: view() and a more specific one sets animation-timeline: --page-scroll, the ident wins wholesale — there is no merging of range or axis from the loser. Keep each element on one strategy; mixed declarations across media queries are where “works on desktop, frozen on mobile” bugs come from.
scroll(self) vs a self-named timeline are not identical in reach. scroll(self) reads the consumer’s own scroll position and only its own; naming the element’s timeline lets other elements read that same scroller. If there is any chance a second consumer appears, name it from the start.
Duplicate names have no anonymous analogue. Two elements declaring --carousel under one timeline-scope null the timeline for everyone — a failure class that simply cannot happen with anonymous functions. Multi-instance components should either stay anonymous or generate per-instance names.
Browser-specific notes
Chrome / Edge (Chromium). Both anonymous functions and named timelines shipped in 115; timeline-scope followed in 116. DevTools support is equal for both forms — getAnimations(), the Animations drawer, and computed styles all report scroll timelines — but only named timelines surface a human-readable ident in the Styles pane, which is a genuine debugging advantage on pages with many scroll-driven effects.
Firefox. Scroll-driven animations are implemented behind a pref (Nightly builds); anonymous and named forms ride the same flag, so there is no strategic reason to prefer one for Firefox compatibility. Guard both with @supports (animation-timeline: scroll()).
Safari. Support is in active development (Technology Preview). The scroll-timeline polyfill handles scroll(), view(), and named timelines; hoisted timeline-scope patterns are its weakest area, which is itself an argument for keeping effects anonymous where the polyfill must carry them.
Feature-detection asymmetry. @supports (animation-timeline: scroll()) passes wherever anonymous timelines work; add the separate @supports (timeline-scope: --check) probe before shipping cross-branch named patterns, per the progressive enhancement guard recipes.
Related
- CSS animation-timeline vs WAAPI ScrollTimeline: Which API When — the same declarative-vs-imperative decision one layer up
- CSS Scroll-Driven Animations vs IntersectionObserver — when neither timeline form is the right tool
- Smooth Parallax Scrolling Without JavaScript — anonymous-timeline parallax that stays deliberately element-local
- The Rendering Pipeline for Scroll Animations — why both forms sample on the compositor and what breaks that
- How to Polyfill Scroll Timeline for Safari — polyfill coverage differences between the two forms