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.
Step-by-step resolution
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,translateandscale, 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.
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.
autodepends 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.
Related
- Scroll Timeline Scoping & Ranges — the parent topic
- Tuning animation-range for Sticky Headers — ranges around sticky UI
- Smooth Scrolling Under Reduced Motion — the scroll-padding shared with anchor links