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 --ident under one timeline-scope is ambiguous — keep names unique per scope.
  • timeline-scope must 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’s view-timeline-name.
  • Naming does not change compositing: a named timeline that animates a layout property still forces main-thread work, so keep keyframes on transform and opacity per compositor-only property optimization.
  • Older engines without native support ignore both forms; ship a CSS.supports('animation-timeline', 'scroll()') guard and a static fallback.