skip to content

In CSS, how do scroll-timeline-name and timeline-scope let an element be animated by a scroll container that is not one of its ancestors?

level: seniorimportance: should knowfreq 20%

answer

  1. anonymous lookup only walks upward
  2. --dashed-ident, like a custom property
  3. visible inside the declaring subtree
  4. hoist the name to a common ancestor
  5. unresolvable means inactive, not default

basics

~20 s

scroll-timeline-name gives a scroller's timeline a --dashed-ident that descendants can reference from animation-timeline. When the animated element is not a descendant, timeline-scope on a common ancestor hoists the name so both sides can see it.

solid answer

~40 s

Anonymous `scroll()` only finds a scroller by walking up from the animated element, so it cannot reach sideways. Named timelines fix that: you put `scroll-timeline: --gallery inline` on the scroll container, which is shorthand for `scroll-timeline-name` and `scroll-timeline-axis`, and then `animation-timeline: --gallery` on the element you want animated. The catch is visibility — the name is resolvable inside the declaring element's subtree, so an indicator that is a *sibling* of the scroller sees nothing, the timeline resolves to inactive, and the animation silently never runs. `timeline-scope: --gallery` on an ancestor that contains both raises the name to that level, making it resolvable throughout that ancestor's subtree. `view-timeline-name` works the same way for view timelines.

code

css · 15 lines
css
.section {
  timeline-scope: --gallery;
}

.gallery {
  overflow-x: auto;
  scroll-timeline: --gallery inline;
}

/* Sibling of .gallery, driven by its horizontal scroll. */
.indicator {
  transform-origin: left center;
  animation: grow-bar linear;
  animation-timeline: --gallery;
}

go deeper

for a junior

Know that a scroll container's timeline can be given a --name and referenced from another element's animation-timeline, rather than only being found automatically.

for a middle

Explain the visibility rule — a name resolves inside the declaring element's subtree — and what timeline-scope does about it, plus the scroll-timeline-name and scroll-timeline-axis longhands behind the shorthand.

for a senior

Diagnose the silent failures: an unresolvable name yields an inactive timeline with no error, a duplicated name inside one scope breaks rather than repeating, and a wrong axis kills the timeline outright.

for a principal

Weigh the coupling this introduces: timeline names are shared identifiers across components, so decide on a naming convention and where scope lives before they leak, or refactors that move markup will break motion silently.

## The limitation named timelines solve `animation-timeline: scroll()` creates an anonymous timeline whose scroller is found by lookup from the animated element: `nearest` walks up the ancestor chain, `self` means the element itself, `root` means the document scroller. All three are vertical relationships. Nothing in that syntax can express "the scroll container over *there*". That matters constantly in real layouts. A scroll indicator sits outside the panel it describes. A progress rail lives in a header while the content scrolls in a `<main>` beside it. A caption animates from an image's visibility while sitting in a different column. ## Naming a timeline You name a timeline on the element that *defines* it — the scroll container for a scroll timeline, the subject for a view timeline. Names are `<dashed-ident>` values, the same `--foo` form as custom properties: ```css .gallery { overflow-x: auto; scroll-timeline-name: --gallery; scroll-timeline-axis: inline; } ``` `scroll-timeline` is the shorthand for those two, so `scroll-timeline: --gallery inline` says the same thing. The view-timeline family mirrors it: `view-timeline-name`, `view-timeline-axis`, `view-timeline-inset`, and the `view-timeline` shorthand. On the consuming side you simply reference the name: ```css .indicator { animation: slide linear; animation-timeline: --gallery; } ``` ## Where the name is visible A timeline name is not global. It is resolvable within the subtree of the element that declared it — descendants can see it, and everything outside that subtree cannot. So the pattern above works when `.indicator` is *inside* `.gallery`, and quietly fails when it is a sibling. "Quietly" is the operative word. An unresolvable name does not raise an error and does not fall back to the document timeline; the animation is attached to an inactive timeline and never progresses. The element just sits at its base styles, which is why this failure eats debugging time. ## timeline-scope hoists the name `timeline-scope` is the escape hatch. Declared on an element with a list of names, it makes those names resolvable throughout that element's subtree, even though the timelines themselves are declared further down: ```css .section { timeline-scope: --gallery; } .gallery { overflow-x: auto; scroll-timeline: --gallery inline; } .indicator{ animation: slide linear; animation-timeline: --gallery; } ``` Now `.gallery` and `.indicator` can be siblings inside `.section`, and both resolve `--gallery` against the same timeline. The rule of thumb: put `timeline-scope` on the nearest element that contains both the timeline's definition and every consumer of it. It accepts a comma-separated list, so one ancestor can hoist several names at once, and `none` is the initial value. ## Failure modes worth knowing - **Name declared but never scoped, consumer outside the subtree.** The classic silent failure above. - **Two elements claiming the same name inside one scope.** With more than one timeline matching a scoped name, the name resolves to an inactive timeline rather than picking a winner — so a name applied to every item in a list, rather than to one element, breaks instead of working "for each". - **The scroller does not scroll on the named axis.** `scroll-timeline-axis: inline` on a container that only overflows vertically yields an inactive timeline for the same reason a bare `scroll()` would. - **The `animation` shorthand written after `animation-timeline`.** It resets the timeline to `auto`, and the symptom — the animation running once on load — looks nothing like a scoping problem. ## Naming versus anonymous Prefer anonymous `scroll()` or `view()` when the relationship really is ancestral, because there is nothing to keep in sync. Reach for names when the two elements are in different branches, when several unrelated elements must share one timeline, or when you want the timeline defined once in a component's root rule and consumed by a part deeper in a different subtree. The cost is a name that has to stay unique within its scope and a `timeline-scope` declaration that has to sit on the right ancestor — both easy to break during a refactor that moves markup around, which is a fair thing to point out in an interview.

  • What happens when animation-timeline references a name that does not resolve?
    The animation is attached to an inactive timeline, so it never progresses and the element keeps its base styles. There is no error and no fall back to the document timeline, which makes a typo in the dashed-ident or a missing `timeline-scope` look identical to "the rule was not applied at all". Check the computed value and the scope ancestor first.
  • Why should you not put the same scroll-timeline-name on every card in a list?
    Because a scoped name that matches more than one timeline resolves to an inactive timeline instead of picking one per element. Names identify a single timeline within a scope, not a template. When each element needs its own timeline, use anonymous `view()` or `scroll()`, which resolve per element by construction.
  • Where exactly should timeline-scope be declared?
    On the nearest common ancestor of the element that defines the timeline and every element that references it. Higher also works but widens the name's reach and raises the chance of a collision with another component using the same ident; lower fails to cover one of the consumers. Treat it as the component root in practice.

saying these in an interview costs you the question

  • Assumes a timeline name is globally available once declared
  • Expects an unresolved name to fall back to the document timeline
  • Puts timeline-scope on the scroll container itself
  • Reuses one timeline name across every item in a list
  • Thinks scroll() can target an arbitrary element by selector

context