view-timeline-inset for Precise Triggers

Part of Scroll Timeline Scoping & Ranges in Modern View Transitions & Scroll APIs.

The problem

Cards fade in as they enter the viewport using animation-timeline: view() and animation-range: entry. On desktop it looks right. On mobile, a 72px bottom navigation bar covers the bottom of the screen, so each card’s entrance animation runs behind the bar and is already finished by the time the card is visible. At the top, a 64px sticky header means exit effects start while the card is still fully readable below the header.

The view timeline measures against the scroll container’s edges. The edges the user can actually see are different.

Root cause analysis: the scrollport is not the visible area

A view progress timeline tracks a subject element’s position relative to its scroll container’s scrollport — the visible rectangle of the scroller. Named ranges such as entry, contain and exit are defined by where the subject’s edges cross the scrollport’s start and end edges on the chosen axis, as described in named animation ranges explained.

Fixed and sticky UI does not change the scrollport. A sticky header inside the document scrolls with the page’s layout but visually covers the top of the root scroller; a fixed bottom bar covers the bottom. The timeline still treats those covered strips as visible.

view-timeline-inset adjusts the scrollport used by the timeline. It takes one or two values: the inset from the start edge and from the end edge of the scrollport, on the timeline’s axis. Positive values shrink the effective area. view-timeline-inset: 64px 72px on a block-axis timeline makes the timeline behave as if the top 64px and bottom 72px were not part of the viewport. Ranges then start and end at the visible edges.

For anonymous timelines, view() takes the same inset. view(block 64px 72px) is an anonymous view timeline on the block axis with those insets.

auto reuses scroll-padding. Sites with sticky headers often already set scroll-padding-top so anchor links land below the header. view-timeline-inset: auto uses the scroller’s scroll padding as the inset, so the header height is declared once.

Percentages refer to the scrollport size. An inset of 10% removes 10% of the scrollport’s size on that axis, useful for effects that should trigger a proportional distance inside the edges rather than a fixed pixel amount.

Where a card's entry range starts, on a 780 px tall phone viewportWithout an inset, entry begins under the 72 px bottom bar.Where a card's entry range starts, on a 780 px tall phone viewportVisible bottom edge708 px from topEntry start, no inset780 px from topEntry start, inset 64px 72px708 px from top
Without an inset, entry begins under the 72 px bottom bar.

Step-by-step resolution

Aligning timelines with the visible viewportDeclare obscured strips once, reuse them everywhere.Aligning timelines with the visible viewport1Measure the sticky header and bottom bar heights and store them as custom properties.--header-h and --bottom-bar-h2Set scroll-padding on the root scroller from the same properties.Anchor links and timelines agree3Use view(block auto) or view-timeline-inset: auto on animated elements.Insets follow scroll-padding4Where auto is unsuitable, pass explicit insets to view().Per-effect precision5Scrub the scroll in DevTools and confirm entries start at the visible edge.No animation hidden behind bars6Under reduced motion, remove scroll-linked effects.
Declare obscured strips once, reuse them everywhere.

Production code pattern

:root {
  --header-h: 64px;
  --bottom-bar-h: 0px;
  scroll-padding-block: var(--header-h) var(--bottom-bar-h);   /* one source for both edges */
}
@media (max-width: 40rem) {
  :root { --bottom-bar-h: 72px; }
}

/* Anonymous timeline: insets come from scroll-padding. */
.card {
  animation: card-reveal linear both;
  animation-timeline: view(block auto);
  animation-range: entry 0% entry 80%;
}
@keyframes card-reveal {
  from { opacity: 0; translate: 0 24px; }
}

/* Named timeline with explicit insets: trigger 15% inside the visible area. */
.chapter {
  view-timeline: --chapter block;
  view-timeline-inset: calc(var(--header-h) + 15%) calc(var(--bottom-bar-h) + 15%);
}
.chapter__marker {
  animation: marker-grow linear both;
  animation-timeline: --chapter;
  animation-range: contain;
}
@keyframes marker-grow { from { scale: 0 1; } }

@media (prefers-reduced-motion: reduce) {
  .card, .chapter__marker { animation: none; }
}

Rendering Impact: composite. Insets change the scroll offsets at which the timeline’s ranges begin and end; the animated properties are still opacity, translate and scale, sampled with scrolling.

Using scroll-padding-block as the single source is the main design win. Anchor navigation, scrollIntoView() and view timelines all agree on where the usable viewport starts and ends, and changing the header height in one media query updates all three.

Card reveal behind a bottom navigation barSame keyframes and range; only the inset differs.Card reveal behind a bottom navigation barview() without insetEntry measured from the screen bottomReveal runs behind the 72px barCard appears already faded inEffect never seenview(block auto)Inset taken from scroll-paddingEntry starts at the bar's top edgeReveal is visibleEffect aligned with view
Same keyframes and range; only the inset differs.

Horizontal and nested scrollers

Insets apply per timeline axis. For a horizontal carousel with a fixed navigation arrow overlaying its right edge, use view(inline 0 56px) so items finish entering before they reach the arrow. For a nested scroller, the inset applies to that scroller’s scrollport, not the page’s, and auto reads its scroll padding. That is usually correct, but if a timeline unexpectedly resolves to an outer scroller the inset will look wrong for reasons unrelated to the inset itself — the lookup rules are covered in nested scrollers and timeline lookup.

Verification checklist

Constraints and trade-offs

  • Insets are static CSS values; dynamically resizing headers need custom properties updated from script.
  • auto depends on scroll padding being set on the right scroller.
  • Large insets can make short subjects never reach the contain range.
  • Browsers without scroll-driven animation support ignore insets along with the timeline.
  • Insets affect range calculations only; they do not change what is painted or clipped.

Frequently asked questions

What does view-timeline-inset do?

It shrinks or offsets the scrollport rectangle that a view timeline measures against, so ranges start and end at the edges you specify instead of the scroller’s actual edges.

How do I set an inset on an anonymous view() timeline?

Pass it as arguments: view(block 64px 72px) sets a 64px start inset and a 72px end inset on the block axis.

What does view-timeline-inset: auto use?

The scroll container’s scroll-padding on that axis, so headers declared for anchor scrolling also adjust timelines.

Can insets be negative?

Yes. Negative insets expand the effective scrollport, which makes ranges begin before the subject reaches the actual edge.