Anchor Positioning Performance During Scroll

Part of CSS Anchor Positioning & Motion in Modern View Transitions & Scroll APIs.

The problem

A data table shows a details popover anchored to the row the user is inspecting, plus tooltips on column headers and a floating action bar anchored to the selected row. Scrolling the table with all three open feels heavier than scrolling it with them closed. On the old JavaScript implementation, scrolling was noticeably worse, so the team wants to know whether anchor positioning actually fixed the problem or just moved it.

Anchored elements are not free, but what they cost, and where, is different from the scroll-listener approach they replace.

Root cause analysis: attachment is layout work, not script work

Scroll listeners cost main-thread script per event. A library that repositions on scroll runs a callback for every scroll event, measures the anchor — forcing layout — and writes inline styles. Because it runs after the scroll has already been composited, the popup lags behind by at least a frame, and on a slow main thread by many.

Anchored elements are positioned by the browser. The anchor relationship is part of layout. When the anchor’s position changes because a scroller moved, the browser updates the anchored element’s position itself, without dispatching events to script. There is no measure-and-write round trip and no forced layout from your code.

That work is not zero. Each anchored element still has a position that depends on another element’s box, so scrolling a container that contains anchors has more to update than one without. In practice, the number of simultaneously anchored elements matters far more than the size of the page: a handful of tooltips and popovers is not measurable, while dozens of anchored badges on every row of a long table can be.

Fallback re-evaluation adds work. An element with position-try-fallbacks may need to re-check whether its current position still fits as the anchor moves through the viewport, and switching produces a visible jump, as covered in position-try fallbacks and motion.

position-visibility reduces work and confusion. anchors-visible hides the anchored element when its anchor is scrolled out of view, which both stops a floating popup drifting over unrelated content and removes it from consideration while hidden.

Main-thread time per second of scrolling with three anchored popups openLong data table on a mid-range laptop, 60 Hz scroll.Main-thread time per second of scrolling with three anchored popups openJS library repositioning on scroll214 msJS library, rAF-throttled96 msCSS anchor positioning11 msNo anchored elements open6 ms
Long data table on a mid-range laptop, 60 Hz scroll.

Step-by-step resolution

Keeping anchored UI cheapFewer anchored elements, less fallback churn, no script.Keeping anchored UI cheap1Remove scroll and resize listeners that repositioned popups.No per-event script2Close or hide anchored popups when they are not needed.Fewer live anchors3Add position-visibility: anchors-visible to anchored overlays.Hidden when the anchor scrolls away4Avoid per-row anchored decorations in long lists.Anchors scale with visible UI, not data5Record a scroll trace with popups open and closed.A measured difference6Do not transition anchored insets while scrolling.
Fewer anchored elements, less fallback churn, no script.

Production code pattern

/* One details popover, hidden when its row scrolls away. */
.row-details {
  position: fixed;
  position-anchor: --active-row;
  position-area: inline-end;
  position-visibility: anchors-visible;         /* no floating popover over unrelated rows */
  position-try-fallbacks: flip-inline;
}

/* Per-row badges: NOT anchored. They live inside the row and scroll with it naturally. */
.row__badge {
  position: absolute;
  inset-block-start: 0.25rem;
  inset-inline-end: 0.5rem;
}

/* Slides between anchors are for selection changes, not for scrolling. */
.tabs__indicator {
  transition:
    left var(--motion-move-duration) var(--motion-move-easing),
    width var(--motion-move-duration) var(--motion-move-easing);
}
@media (prefers-reduced-motion: reduce) {
  .tabs__indicator { transition: none; }
}
// Only one row is anchored at a time.
function setActiveRow(row) {
  document.querySelector('[style*="--active-row"]')?.style.removeProperty('anchor-name');
  row.style.anchorName = '--active-row';
}

Rendering Impact: layout updates as anchors move during scroll, handled by the browser; no script per scroll event. Keeping badges inside their rows means they are ordinary in-flow elements with no anchor relationship to maintain.

The single-active-anchor pattern is the main lever. Anything that could be positioned inside the element it relates to should be, leaving anchor positioning for genuinely detached UI: popovers, tooltips, menus and toolbars that must escape a clipping container or the stacking context.

When to anchor and when not toAnchor detached UI; keep in-flow decoration in flow.When to anchor and when not toAnchor it?WhyTooltip escapingoverflowYesMust leave the clipping containerDropdown menu inthe top layerYesTop layer has no layout relationshipBadge on a cardcornerNoPosition it inside the cardPer-row status dotNoScrolls with the row anywaySelection toolbarYesFollows an arbitrary target
Anchor detached UI; keep in-flow decoration in flow.

Measuring it yourself

Open the page with anchored elements visible and record a Performance trace while scrolling for a few seconds, then repeat with them closed. Compare total Layout time and the number of layout events in the Main track. A meaningful difference shows up as extra layout work per scroll frame; anything below a millisecond per frame is noise. If the difference is large, check how many elements are anchored at once — the count, rather than the page’s size, is almost always the cause. The same trace also shows whether any script is still repositioning elements, which appears as yellow blocks correlated with scroll events, the signature described in debugging jank with the Long Animation Frames API.

Verification checklist

Constraints and trade-offs

  • position-visibility: anchors-visible hides content abruptly; that is usually better than a detached overlay, but it is a jump.
  • Fallback re-evaluation during scroll can cause flips near viewport edges.
  • Fixed-position anchored elements interact with scroll containers differently from absolutely positioned ones; test both.
  • Implementation maturity varies; measure in each target browser rather than assuming.
  • Replacing a library wholesale removes its edge-case handling, such as virtual anchors at text ranges.

Frequently asked questions

Is CSS anchor positioning faster than a JavaScript positioning library?

For staying attached during scroll, yes: the browser updates positions as part of layout instead of running script for every scroll event, and there is no forced layout from measurement.

How many anchored elements can a page have?

A handful of open popups is not measurable. Avoid anchoring per-row or per-item decorations in long lists; position those inside their items instead.

What does position-visibility: anchors-visible do?

It hides the anchored element while its anchor is scrolled out of view, preventing detached overlays and removing the element from consideration while hidden.

Should anchored elements animate while scrolling?

No. Keep slides between anchors for discrete changes such as selection; animating insets during scroll adds layout work to every scroll frame.