Tech · · 8 min read
Why the page jumps when an image loads, and the browser feature that stops it
Safari 27 enabled scroll anchoring, so every engine now has it. How the browser picks an anchor, the eight things that switch it off, and when to opt out.
You are halfway down an article, an image above the viewport finishes loading, and the text you were reading slides down the screen. Scroll anchoring is the browser feature that stops that happening — it picks a node you can see, and when that node moves because of a layout change above it, the browser moves the scroll position by the same amount so the node stays put.
It has been in Chrome since version 56 and Firefox since 66. Safari 27, released on 14 September 2026, "enabled scroll anchoring, which prevents visible jumps in scroll position when content is inserted or removed above the viewport". That closes the last gap: every major engine now does it, by default, with no opt-in.
Which raises the more useful question. It is on by default and has been for years, so why do pages still jump? Almost always because something in the page has tripped one of the conditions that switch it off. Those conditions are written down, and they are the interesting part.
drafts.csswg.org ↗CSS Scroll Anchoring Module Level 1The normative definition, including the suppression triggers. Source: CSS Working GroupHow the browser picks the anchor
Each scrolling box tries to select one anchor node — a single element whose movement it will compensate for. The selection algorithm is short:
- If the scroller has
overflow-anchor: none, pick nothing. - If the scroller is not scrolled away from the origin in its block direction, pick nothing. More on this below — it matters more than anything else in the spec.
- Otherwise check the priority candidates first: the focused element if it is text-editable, and the element containing the current find-in-page match.
- Otherwise walk the children, descending into any node that is only partly visible, and pick the first fully visible node found.
Step 4 is why it prefers deep nodes. The spec explains the reasoning: a deep anchor "minimises the possibility of content changing inside the anchor node but outside the viewport, which would cause visible content to shift without triggering any scroll anchoring adjustment". An anchor that is a whole <article> is useless if the change happens inside that article.
Some subtrees are skipped entirely when looking for an anchor:
display: noneposition: fixedposition: stickyposition: absolutewhere the containing block is an ancestor of the scrolling box- anything with
overflow-anchor: none
The fixed, sticky and absolute exclusions are the reason a sticky header never becomes the anchor, which is what you want: it does not move when content above it changes, so it would compensate for nothing.
It does nothing at the top of the page
Step 2 of the algorithm is worth reading twice. If the scroller is at its origin — the user has not scrolled — there is no anchor node and there is no adjustment. The same fact appears again in the list of suppression triggers: "the scroll offset of the scrollable element being zero" suppresses adjustments.
This gets scroll anchoring described as a fix for layout shift, and it is not one. A visitor landing on your page and watching a web font swap, a banner insert, or an unsized hero image push everything down is at scroll offset zero. Scroll anchoring will not lift a finger. It only helps someone who has already scrolled.
Reserving space — width and height on images, aspect-ratio, min-height on slots that fill in later — is still the actual fix for content moving. Scroll anchoring is a safety net for everything you did not manage to reserve space for, and only for readers who are already reading.
The suppression window, and why your infinite scroll still jumps
When an anchor node moves, the browser does not adjust immediately. It computes the offset difference and queues the adjustment until the end of the current suppression window.
The window begins at the start of the current event-loop iteration (or the end of the last window) and ends at whichever comes first:
- the end of that event-loop iteration, or
- immediately before any operation whose result would differ because of a change in scroll position — the spec's own example is a call to
getBoundingClientRect().
That second clause is the one to watch. A typical infinite-scroll or "load more" implementation inserts content and then measures something — a height, a bounding rectangle — to decide what to do next. The measurement closes the window there and then, so the queued adjustment is applied before your code reads its number back. The rectangle you measure already includes the scroll correction, and any scrolling you do on the basis of it is applied on top.
Anything in this list, happening within the window on any element on the path from the anchor node up to the scroller, suppresses the adjustment entirely:
| Trigger | Typical cause |
|---|---|
A change to top, left, right or bottom | Repositioning a panel as new content arrives |
A change to margin or any longhand | Collapsing a spacer, a "loading" margin |
A change to padding or any longhand | Swapping a skeleton for real content with different padding |
A change to width, height, min-width, max-width, min-height or max-height | Animating a container open, setting an explicit height after measuring |
A change to position | Promoting an element to absolute while it animates |
A change to transform | Almost any JavaScript animation library |
| An element anywhere in the scroller becoming, or ceasing to be, absolutely positioned | Modals, drag-and-drop, virtualised lists |
| The scroll offset being zero | The reader is at the top |
Note the scope difference in the second-to-last row: that trigger applies "regardless of whether the modified element is on the path from the anchor node to the scrollable element". A drag interaction somewhere unrelated in the same scroller can suppress anchoring for the thing you were reading.
The spec is candid that these exist for compatibility rather than elegance: they were added "for compatibility with existing web content that has negative interactions with scroll anchoring due to shifting content in scroll event handlers".
The practical version: if you are inserting content above the viewport and the page still jumps, look for a style change on an ancestor in the same frame. Moving that change into a different frame, or doing it without touching the listed properties, is usually what fixes it.
When to switch it off on purpose
overflow-anchor has two values and one job.
.chat-log {
overflow-anchor: none;
}| Value | Meaning |
|---|---|
auto | The element may participate in anchor selection. This is the initial value. |
none | The element and its descendants are not eligible as anchors for this scroller or any ancestor scroller. |
Three cases where none is the right call:
- A log or chat pane pinned to the bottom. You want new messages to push the view, not to have the browser hold the old ones in place. Anchoring fights your own scroll-to-bottom logic.
- A decorative region that changes constantly — a ticker, an animated ad slot — sitting inside the reading column. Excluding it stops the browser choosing it as the anchor and then compensating for its every twitch.
- A scroller where you do your own scroll management and the anchoring adjustment is producing scroll events you then react to. The adjustment is defined as ordinary scrolling and does fire
scrollevents.
One asymmetry to know: you cannot turn it back on. The spec is explicit that there is no way to re-enable scroll anchoring for the descendants of an overflow-anchor: none element — the only exception is that a descendant scroll container gets it back for its own scrolling box, unless it sets none too. So apply none to the smallest element that solves the problem, never to body.
Scroll snapping wins
If a scroll container is currently snapped to an element, scroll anchoring is limited to adjustments that re-snapping would allow. In a snapping carousel or a full-page-section layout, expect anchoring to do less than you would predict, because the snap position is the stronger constraint.
Browser support
At the time of writing, from browser-compat-data:
| Browser | overflow-anchor |
|---|---|
| Chrome and Edge | Chrome 56, Edge 79 |
| Firefox (desktop and Android) | 66 |
| Safari (macOS and iOS) | 27 |
Two caveats about reading that table. Scroll anchoring itself is a default behaviour, not a property — overflow-anchor support is the best available proxy for it, since a browser that implements the opt-out implements the behaviour. And because Safari 27 shipped this week, the Baseline datasets lag: web-features 3.38.0, the current release at the time of writing, still records no Safari support for overflow-anchor. A Baseline badge is only ever as current as its data, which is one of the limits worth knowing about it.
A browser without support ignores overflow-anchor and does not anchor, which is the behaviour every browser had until 2017. There is nothing to polyfill and nothing to guard.
FAQ
Is scroll anchoring the same as scroll-behavior: smooth?
No. scroll-behavior controls how a scroll you asked for is animated. Scroll anchoring silently changes the scroll offset to cancel out a layout change you did not ask for.
Will scroll anchoring fix my Cumulative Layout Shift score?
Do not count on it. Anchoring does nothing at scroll offset zero, which is where a visitor is during most of a page load. Reserve space for images, ads and embeds instead.
Does it work inside a scrollable div, or only the page?
Both. The algorithm runs per scrolling box, so a scrollable div selects its own anchor independently of the document.
Can I detect that an adjustment happened?
Not directly. The adjustment is defined as scrolling, so it produces scroll events, but there is no flag marking a scroll event as anchoring-induced.
Why does it stop working when I animate something?
Because a change to transform, margin, padding, position or a size property on the path from the anchor to the scroller suppresses the adjustment for that window. Animation libraries touch those properties constantly.
Sources
- CSS Scroll Anchoring Module Level 1 — CSS Working Group editor's draft, for the selection algorithm, suppression triggers and
overflow-anchor - Safari 27 release notes — Apple, released 14 September 2026
@mdn/browser-compat-data— per-browser version data- web-features — Baseline status
More to read
- Design
Design · · 7 min read
The missing space between 日本語 and English, and the CSS that adds it
One declaration adds the thin space typographers put between ideographs and Latin letters. Why it is off by default in every engine, and which keywords actually work.

Design · · 6 min read
Text that sits flush in its box: text-box-trim in every engine
Firefox 154 shipped text-box-trim in August 2026, so all three engines now trim the space above and below text. The keywords, and when it does nothing.