Named vs Anonymous Scroll Timelines
Part of Scroll Timeline Scoping & Ranges in Modern View Transitions & Scroll APIs.
The problem
CSS scroll-driven animation offers two ways to reference a scroll source, and picking the wrong one is the most common reason a timeline appears to do nothing. The anonymous form — animation-timeline: scroll() or view() — resolves against the element’s own nearest scroller or its own visibility, with no declaration required. The named form — scroll-timeline-name / view-timeline-name on a source, referenced by a --custom-ident on the consumer — lets an arbitrary element read a specific, possibly distant, scroll source. The decision hinges entirely on where the animated element sits relative to the scroll source in the DOM.
Root-cause analysis: how name resolution works
animation-timeline: scroll() is re-evaluated for each element that carries it. The browser walks up from that element to find the nearest scroll container matching the requested axis and binds to it. Nothing is shared: two elements with identical scroll() values may bind to two different scrollers. view() behaves analogously but measures the element’s own intersection with its scrollport, so it is inherently per-element.
Named timelines invert the flow. The source element publishes a name; a consumer subscribes by identifier. Resolution is a lookup up the consumer’s ancestor chain for a matching name. If the source is not an ancestor, the lookup fails and animation-timeline collapses to none, leaving the animation stuck at its fill value. timeline-scope repairs this by declaring the name on a chosen ancestor of both the source and the consumer, so a name published deep in one subtree becomes visible in a sibling subtree. Because all of this is resolved at style-computation time, the runtime path is identical to the anonymous case: a compositor-sampled progress value, as detailed in the parent guide on scroll timeline scoping and ranges.
Decision matrix
| Situation | Use | Why |
|---|---|---|
| The animated element is the scroll container, or a descendant of it | Anonymous scroll() |
Nearest-scroller resolution finds the source automatically; no name needed |
| The animated element is a descendant of the subject whose visibility drives it | Anonymous view() |
view() measures the element’s own intersection; ideal for self-revealing cards |
| A fixed progress bar is a sibling of the scrolled article | Named scroll-timeline-name + timeline-scope |
The consumer is outside the source subtree, so the name must be hoisted |
| Many elements must read from one identical source | Named timeline | Anonymous timelines re-resolve per element; a shared name guarantees one source |
| A parent must react to a child’s visibility | Named view-timeline-name + timeline-scope |
The consumer (parent) is an ancestor of the source (child), so the name is hoisted to the parent |
| Prototype or single self-contained component | Anonymous | Least markup; no name-collision risk across the page |
Production code
The block below shows the named pattern in full: one scroll source, two independent consumers in a different subtree, each targeting its own range, all gated for reduced motion.
/* Intent: one article scroller drives a sibling progress bar AND a sibling
chapter marker, using a hoisted name so both siblings can subscribe. */
/* Common ancestor hoists the name so it is reachable across subtrees. */
.reader {
timeline-scope: --read-progress;
}
/* Source: publishes the named scroll timeline. */
.reader__article {
overflow-y: auto;
scroll-timeline: --read-progress block; /* name + axis shorthand */
}
/* Consumer A: a sibling progress bar, not a descendant of the article. */
.reader__bar {
transform-origin: left center;
transform: scaleX(0);
animation: grow linear both;
animation-timeline: --read-progress; /* subscribes by name */
animation-duration: auto; /* required for scroll timelines */
}
/* Consumer B: a sibling marker that fades in over the last quarter. */
.reader__marker {
opacity: 0;
animation: appear linear both;
animation-timeline: --read-progress;
animation-range: 75% 100%; /* only the final stretch */
animation-duration: auto;
}
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes appear { from { opacity: 0; } to { opacity: 1; } }
/* Accessibility gate: sever the scroll binding, settle to resting state. */
@media (prefers-reduced-motion: reduce) {
.reader__bar,
.reader__marker {
animation: none;
animation-timeline: auto;
transform: scaleX(1); /* bar shown complete */
opacity: 1; /* marker shown */
}
}
Rendering Impact:
transform+opacity— composite only. The hoisted name resolves once at style time; both consumers sample the same compositor timeline with no per-frame main-thread work.
Verification checklist
Constraints and trade-offs
- Named timelines add a name-collision surface: two sources publishing the same
--identunder onetimeline-scopeis ambiguous — keep names unique per scope. timeline-scopemust be an ancestor of both source and consumer; placing it too low silently breaks resolution with no console error.- Anonymous
view()cannot be shared: if you need several elements to react to one element’s visibility, you must name that element’sview-timeline-name. - Naming does not change compositing: a named timeline that animates a layout property still forces main-thread work, so keep keyframes on
transformandopacityper compositor-only property optimization. - Older engines without native support ignore both forms; ship a
CSS.supports('animation-timeline', 'scroll()')guard and a static fallback.
Related
- Scroll Timeline Scoping & Ranges — the parent guide covering
timeline-scopeand theanimation-rangephases in depth - Scroll-Driven Animation Patterns — the production patterns that consume these timelines
- Accessible Motion Architecture — reduced-motion strategy for every scroll-linked animation