skip to content

In CSS scroll-driven animations, what is the difference between the scroll() and view() values of animation-timeline, and when would you pick each?

level: middleimportance: should knowfreq 34%

answer

  1. container progress versus element progress
  2. one timeline shared, or one per element
  3. subject box crossing the scrollport
  4. reading bar versus card reveal
  5. view() takes an axis and an inset

basics

~20 s

scroll() tracks how far a scroll container has been scrolled, so progress is the same for every element using it. view() tracks one specific element's journey through the scrollport, so each element progresses on its own schedule.

solid answer

~40 s

Both create progress-based timelines, but they measure different things. `scroll()` is a *scroll progress timeline*: 0% is the container scrolled to the start, 100% scrolled to the end. It describes the container, so every element attached to it moves in lockstep — that is what you want for a reading-progress bar or a header that shrinks as the page scrolls. `view()` is a *view progress timeline*: 0% is the subject element just beginning to enter the scrollport, 100% is it having fully left. It describes one element's trip past the viewport, so ten cards each get their own timeline and each animates as it arrives. Reach for `view()` for reveal-on-scroll and parallax per item, and pair it with `animation-range` to choose which slice of that trip counts.

code

css · 11 lines
css
@keyframes fade-up {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: none; }
}

/* One rule, but each card gets its own timeline. */
.card {
  animation: fade-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

go deeper

for a junior

Know that scroll() follows the container's scroll position while view() follows a single element as it passes through the visible area, and that a reveal-on-scroll effect uses view().

for a middle

Explain the measurement behind each: the scroller's scrollable range versus the subject box crossing the scrollport, and why one produces shared progress and the other per-element progress.

for a senior

Show judgment in picking between them for a real effect, and name the practical traps: the nearest scroll container being an inner panel, the default range spanning the element's whole passage, and the need for a fill mode.

for a principal

Frame it as a design decision about coupling: container-driven effects give the page one shared sense of position, element-driven effects scale to unknown list lengths without any per-item wiring or script.

## The question each one answers The two anonymous timeline functions differ in what they are measuring, not in how they are wired up. - `scroll()` answers **"how far has this container been scrolled?"** - `view()` answers **"where is this element in its trip through the scrollport?"** Everything else follows from that. ## scroll(): one timeline for the whole container A scroll progress timeline maps the scroll container's position across its scrollable range onto 0%–100%. It knows nothing about the animated element — the element could be off-screen the whole time and the timeline would still advance. Because the measurement belongs to the container, every element that attaches to the same scroller shares identical progress. That makes it the right tool whenever the animation is *about the page*: a progress indicator, a sticky header that compacts, a background hue that shifts as you descend, a table-of-contents marker. ```css #progress { animation: grow-bar linear; animation-timeline: scroll(root block); } ``` ## view(): one timeline per element A view progress timeline is defined by the relationship between a **subject** box and the **scrollport** of its scroll container. Progress runs from the moment the subject's box first begins to intersect the scrollport to the moment it has completely passed out of it. With anonymous `view()`, the subject *is* the element carrying the declaration — which is exactly why a single class can drive per-element behaviour: ```css .card { animation: fade-up linear both; animation-timeline: view(); } ``` Every `.card` gets its own timeline. The first card is already halfway through its range when the tenth has not started. No JavaScript, no per-element bookkeeping, no observer callbacks. `view()` takes optional arguments too: `view(<axis> <inset>)`, where the axis is `block` (default), `inline`, `x`, or `y`, and the inset shrinks or grows the region of the scrollport that counts as "in view" — `view(block 20%)` treats the middle band of the viewport as the visible region, so a reveal fires before the element touches the true edge. ## The subject can be a different element Anonymous `view()` always makes the declaring element the subject. When you want element A's animation driven by element B's visibility, you name the timeline instead: put `view-timeline: --hero block` on B and `animation-timeline: --hero` on A. The named form also has `view-timeline-name`, `view-timeline-axis`, and `view-timeline-inset` longhands. ## Choosing between them Ask whether the animation should be identical for every element using it. | Intent | Timeline | |---|---| | Reading progress bar | `scroll(root block)` | | Header shrinks as the page scrolls | `scroll(root block)` | | Each card fades in as it arrives | `view()` | | Image parallaxes within its own frame | `view()` | | Indicator tracks a horizontal carousel | `scroll(self inline)` on the scroller, or a named scroll timeline | A useful cross-check: if the effect would look wrong when two instances animate at different moments, you want `scroll()`. If it would look wrong when they animate *together*, you want `view()`. ## Shared gotchas Both need a real scroll container. `view()` measures against the scrollport of the nearest ancestor scroll container on the chosen axis, so an element inside an `overflow: auto` panel is measured against that panel, not the viewport — surprising when the panel is small and the effect never seems to complete. With `view()` you almost always want `animation-fill-mode: both` (the `both` keyword in the `animation` shorthand). Without it, the element snaps back to its unanimated styles once it has passed out of the range, which for a fade-in reveal means the element disappearing again after it has scrolled by. Finally, `view()`'s default range covers the subject's entire passage — from first entering to fully leaving. A reveal that should finish once the element is comfortably on screen needs `animation-range` to narrow that, otherwise the fade only reaches full opacity as the element exits the far edge, which reads as an effect that never quite lands.

  • With view(), which element is the subject whose visibility drives the timeline?
    The element the declaration sits on. Anonymous `view()` always makes the declaring element its own subject, which is what lets a single class give every card an independent timeline. To drive one element's animation from a different element's visibility, name the timeline with `view-timeline` on the subject and reference that name from `animation-timeline`.
  • Why does a view()-driven fade-in reveal often need animation-fill-mode: both?
    Because outside the animation's range the element falls back to its unanimated styles. Without a fill, an element that has scrolled past the end of its range snaps back to `opacity: 0` and vanishes. `both` holds the first keyframe before the range and the last keyframe after it, so the reveal stays revealed.
  • What does the inset argument in view(block 20%) change?
    It shrinks the region of the scrollport that counts as visible, insetting each edge by 20%. The subject is then treated as entering when it crosses that inset line rather than the true viewport edge, so a reveal completes while the element is comfortably on screen instead of at the very edge. A negative inset expands the region outward.

saying these in an interview costs you the question

  • Thinks view() is just scroll() scoped to an element's ancestor
  • Expects every element sharing a view() rule to animate simultaneously
  • Assumes view() measures against the viewport even inside a scrolling panel
  • Omits fill-mode and wonders why the revealed element disappears again
  • Believes view() needs an observer callback to work

context