Home / Articles / Performance /

performancerendering

content-visibility: skipping off-screen rendering

One declaration tells the browser not to lay out or paint content the reader cannot see — and one more declaration keeps the scrollbar from lying about it.

Paying to render what nobody sees

A browser lays out and paints the whole document, not the part of it in the viewport. On a 200-comment thread or a documentation page with forty collapsed sections, the first frame is delayed by work whose output the reader will never look at. Virtualised lists solve this in JavaScript by removing off-screen DOM, at the price of broken find-in-page, broken anchor links, and a scroll position that drifts.

content-visibility moves that decision into CSS. The DOM stays intact; the browser skips the rendering work for subtrees that are not near the viewport, and does it again in reverse as they approach. Nothing is removed, so the behaviours that virtualisation breaks keep working.

The three values

visible is the initial value and changes nothing. The other two do the work:

  • auto — turns on layout, style, and paint containment permanently, and additionally skips the element's contents whenever the element is not relevant to the reader.
  • hidden — always skips the contents, regardless of scroll position, until you change the declaration.

An element counts as relevant when it is close to the viewport, when it or its contents are focused or selected, or when it is placed in the top layer. "Close to the viewport" is deliberately loose in the spec — engines render a margin beyond the visible area so scrolling does not reveal blank space.

While contents are skipped, the browser additionally applies size containment, does not paint the descendants, does not hit-test them, and does not advance CSS transitions or animations on them. Skipped text also drops out of innerText, which is a real trap for scripts that read layout-derived text.

contain-intrinsic-size is not optional

Size containment is the catch. Once the contents are skipped, the element stops deriving its height from them, so it collapses. A page full of collapsed sections has a scrollbar that grows every time you scroll — the classic symptom of shipping content-visibility: auto on its own. contain-intrinsic-size supplies the placeholder dimensions the browser should assume instead:

.thread-item {
  content-visibility: auto;
  /* remember the real size once rendered; guess 320px until then */
  contain-intrinsic-size: auto 320px;
}

The auto keyword in front of the length is the important part. Without it, the element is pinned at 320px forever, including after it has been rendered once. With it, the browser records the last rendered size and reuses that value when the element is skipped again, so scroll-height estimates converge on the truth as the reader moves down the page.

Pick the placeholder length from real content: measure a handful of typical items and use the median. A wildly wrong guess produces the same scrollbar jitter as no guess at all.

hidden versus display: none

content-visibility: hidden looks like display: none from the outside, and the spec even notes the resemblance. The difference is what happens on the way back. An element hidden with display: none has no boxes at all, so revealing it is a full layout from scratch. An element with content-visibility: hidden keeps its rendering state cached, and the browser can restore it far more cheaply.

/* Inactive tab panels: cheap to hide, cheap to bring back */
.tab-panel:not(.is-active) {
  content-visibility: hidden;
}

That makes it a good fit for anything that toggles repeatedly — tab panels, accordion bodies, wizard steps — and a poor fit for content that is shown once or never, where display: none costs less memory.

What stays reachable, and what does not

The two values differ sharply on accessibility, and the difference decides which one you can use.

With auto, skipped contents must remain available to user-agent features: find-in-page locates them, tab order reaches them, and they stay selectable and focusable. Off-screen content also remains in the DOM and in the accessibility tree. Focusing or selecting something inside a skipped subtree makes the element relevant again, which forces it to render.

With hidden, none of that holds. The spec requires that skipped contents not be accessible to find-in-page or tab-order navigation, and not be selectable or focusable. That is correct for a closed tab panel and wrong for a long article that a reader may want to search. Choosing hidden for off-screen document content is the most common way this property gets misused.

Browser support and strategy

The property is supported in Chrome 85+, Firefox 125+, and Safari 18+. Support for the auto value is newer than that headline suggests: per MDN's compatibility data, Safari 18 through 25 implemented it only partially — skipped content was not findable via find-in-page — and the complete behaviour arrived in Safari 26. contain-intrinsic-size is supported in Chrome 83+, Firefox 107+, and Safari 17+, with the auto <length> form in Chrome 117+, Firefox 117+, and Safari 17+.

Browsers without support ignore the declarations and render everything, which is the correct fallback: slower, never broken.

  • Apply auto to repeated siblings — list items, cards, comment entries, article sections — not to a single wrapper around the whole page. One giant contained element skips nothing useful.
  • Always pair it with contain-intrinsic-size: auto <length>.
  • Do not put it on elements that are always in view, such as a header or hero. The containment is permanent, the skipping never happens, and you have only added constraints.
  • Verify with find-in-page on a real device before shipping, and re-check anchor links into deep sections.
  • For the containment primitives underneath this property, see CSS containment.