Overflow, Clipping and Scrollport Requirements for Timelines
Whether an element can supply a scroll timeline is decided by the same rules that decide whether it can scroll at all — and those rules are less obvious than they look. overflow: hidden establishes a scroll container; overflow: clip does not. Padding on the container changes what a view timeline measures against. And a fixed header overlapping the scrollport will make reveals fire where nobody can see them. This guide covers all three. It sits under timeline attachment and scroll container mechanics.
When to use this approach
Read this when any of these describe your situation:
- A container that visually clips its content is not driving a timeline you attached to it.
- Reveals fire slightly too early or too late relative to where the element appears on screen.
- A sticky or fixed header overlaps content, and animations behind it have already finished by the time the reader sees the element.
- You are choosing between
overflow: hiddenandoverflow: clipon a container that also needs to drive an animation.
Implementation
1. Choose an overflow value that establishes a scroll container
/* drives a timeline — a scrollable range exists, even with no visible scrollbar */
.masked-panel { overflow: hidden; block-size: 40vh; }
/* does NOT drive a timeline — clip removes scrollability entirely */
.clipped-panel { overflow: clip; block-size: 40vh; }
clip is the newer value and is frequently recommended as the modern replacement for hidden, because it clips without creating a scroll container — which is exactly why it cannot supply a timeline. When a container needs both clipping and a timeline, hidden is the correct choice.
2. Give the container a definite size
.panel {
block-size: 60vh; /* or a max-block-size, or a grid track that constrains it */
overflow-y: auto;
min-block-size: 0; /* flex and grid items need this to stop stretching */
}
Without a constrained size the element grows to fit its content, never overflows, and has no scroll range regardless of its overflow value. In flex and grid layouts the default min-block-size: auto is what causes the stretch, and setting it to zero is the standard fix.
3. Understand what the scrollport measures
A view timeline measures a subject’s progress against the scroll container’s scrollport — its padding box — rather than its border box. Padding on the container therefore shifts every measurement inward, which is usually invisible and occasionally exactly what you need.
4. Use an inset to allow for overlapping chrome
.reveal {
animation: fade-up linear both;
animation-timeline: view();
/* the site header is 64px tall and overlaps the scrollport;
start measuring below it */
view-timeline-inset: 64px 0;
}
The inset shrinks the box used for measurement without touching layout. A positive value pulls the measurement line inward; a negative one pushes it outward, which is occasionally useful for starting an animation slightly before an element is technically visible.
5. Set the inset from the same token as the chrome
:root { --header-block-size: 64px; }
.site-header { block-size: var(--header-block-size); }
.reveal {
view-timeline-inset: var(--header-block-size) 0;
}
Deriving both from one value means a header height change does not silently desynchronise every reveal on the site.
Verification
Confirm the container scrolls before investigating anything else. In the console, compare scrollHeight with clientHeight on the element you believe is the scroller. If they match, the overflow value or the sizing is the problem, and no amount of timeline configuration will help.
Then check where the animation actually starts. Scroll slowly and watch for the moment the effect begins; if that moment is behind fixed chrome, an inset is the fix. Comparing against a temporary outline on the subject makes the start point much easier to see than watching the animation itself.
Finally, test at your narrowest viewport and with the browser’s toolbar in its collapsed state on mobile. Both change the effective scrollport height, and an inset tuned only at desktop will be wrong on a phone where the header collapses.
Edge cases and gotchas
overflow: hidden on one axis implies scrollability on both. Setting overflow-y: hidden with overflow-x: visible computes the visible value to auto, because the two axes cannot mix visible with a clipping value. The result is a container that scrolls horizontally when you expected neither axis to scroll.
overflow: clip combined with overflow-clip-margin. The margin controls how far beyond the box painting extends, and has no bearing on scrollability. An element with clip remains unable to drive a timeline no matter what margin it carries.
Padding on the scroller shifts view-timeline measurements. Because the scrollport is the padding box, adding padding to a container moves the measurement lines inward by that amount for every subject inside it. This is a reasonable way to add breathing room to reveals globally, and a confusing source of drift if it is added for visual reasons and nobody connects the two.
scroll-padding is not the same thing. scroll-padding affects where scroll-snap and anchor navigation come to rest; it does not change what a view timeline measures. Use view-timeline-inset for the timeline and scroll-padding for the landing position — they frequently want the same value for the same reason, but they are separate properties.
Insets accept percentages. A percentage inset resolves against the scrollport size on that axis, which makes view-timeline-inset: 10% a reasonable viewport-relative way to keep reveals away from the very edges without hard-coding pixel values.
Browser-specific notes
overflow: clip support is broad across all three engines, and all three agree that it does not establish a scroll container. The value is recent enough, though, that codebases still contain hidden where clip was intended and vice versa, so the distinction is worth auditing rather than assuming.
Safari’s early view() builds had parsing bugs around the inset argument in the functional form, which is one reason view-timeline-inset as a separate property is the more portable way to express it. Current builds parse both correctly.
Chromium and Firefox both compute the scrollport from the padding box as specified. Where the engines visibly differ is in mobile viewport behaviour: a collapsing browser toolbar changes the effective scrollport height mid-scroll, and the engines resize at slightly different moments, which can shift where a reveal fires by a few pixels. An inset large enough to absorb that difference is more robust than one tuned to the exact chrome height.
Frequently Asked Questions
Should I use hidden or clip for a container that clips a parallax layer?
clip, unless that same container drives a timeline. clip is cheaper — it creates no scroll container and no scrollable range — and it avoids the class of bug where content is accidentally scrolled into view by a focus event inside a hidden container. When the container must both clip and supply progress, hidden is the only option of the two.
Does an inset change where the element is, or only where the animation runs?
Only the measurement. Layout, painting and hit-testing are untouched; the element sits exactly where it did. This is what makes insets the right tool for compensating for overlapping chrome — the alternative, adding padding or margins, would move the content itself.
Can an inset be negative?
Yes, and it enlarges the measured box beyond the scrollport. That starts a view timeline slightly before the subject is technically visible, which is occasionally useful for effects that should already be underway by the time the element appears. Large negative insets make the animation finish while the element is still well inside the viewport, which usually reads as the effect ending too early.
Why does my reveal fire twice at the top of the page?
Usually because the subject begins the page already intersecting the scrollport, so its view timeline starts at a non-zero progress, and a mobile toolbar collapse then resizes the scrollport and re-derives it. Setting animation-fill-mode: both and choosing a range that starts after the subject has genuinely entered removes the visible jump.
Related
- Why a scroll timeline resolves to none — the diagnostic path when the container is not driving anything
- Which element becomes the scroller — how the ancestor walk picks a container
- Understanding the CSS Scroll-Timeline API — ranges, insets and the timeline functions
- Sticky Header & Navigation Transitions — the chrome that most often needs an inset