Guide

CSS content-visibility explained

Harbor Commerce publishes a 12,000-word industrial-equipment buyer’s guide as one static HTML page with 38 chapter sections, comparison tables, and embedded spec sheets. On a mid-range Android phone the browser spent 890 ms laying out and painting content the user would not see for another dozen scroll gestures. INP on the sticky filter bar suffered because the main thread was saturated before the first interaction could respond. Applying content-visibility: auto to each <section class="chapter"> — paired with contain-intrinsic-size placeholders — let Chromium skip rendering work for off-screen subtrees until they approached the viewport. Initial render time dropped 71%, LCP improved from 3.4 s to 2.1 s, and scroll remained smooth because the browser promoted sections lazily on the compositor thread. This guide explains how CSS content visibility works, the three keyword values, sizing with contain-intrinsic-size, accessibility and find-in-page behavior, pairing with Intersection Observer and virtual scrolling, the Harbor Commerce refactor, a technique decision table, pitfalls, and a production checklist.

What content-visibility actually skips

Browsers normally perform style, layout, and paint for every element in the document tree, even nodes far below the fold. On long pages — documentation sites, news articles with comment threads, e-commerce category pages with hundreds of cards — that upfront work delays first paint and competes with JavaScript hydration for the main thread.

The content-visibility property tells the user agent it may defer rendering of an element’s contents when they are not needed for the user’s immediate view. Skipped subtrees still exist in the DOM (unlike display: none), but the engine avoids layout and paint until the subtree becomes relevant — typically when it is near or inside the viewport.

The property works best on independent sections: article chapters, product cards in a grid, FAQ accordions, dashboard widgets, or comment blocks. Each section should be a self-contained subtree that does not affect siblings’ layout when skipped. Combine with contain: layout style paint (or the shorthand content-visibility: auto which implies containment) so skipped sections do not trigger full-document reflow.

The three values: auto, hidden, and visible

content-visibility: auto

The production default. The browser skips rendering when the element is off-screen and renders it when the user scrolls near. Search indexing and accessibility APIs can still access the content; find-in-page (Ctrl+F) forces rendering of matching sections in supporting browsers. Use on repeated page sections where approximate height is known or estimable.

content-visibility: hidden

Skips rendering unconditionally while preserving layout size if contain-intrinsic-size is set. Unlike visibility: hidden, hidden content is not painted at all — useful for tab panels, carousel slides, or accordion bodies that should not consume GPU memory until activated. Pair with JavaScript or the :target pseudo-class to flip to visible when shown.

content-visibility: visible

Explicit opt-in to normal rendering. Useful inside a parent with auto when one child must always paint (e.g. a sticky chapter nav).

contain-intrinsic-size: placeholders without layout shift

Skipping layout removes the element’s contribution to document height until it renders — which causes cumulative layout shift when the user scrolls and sections expand. contain-intrinsic-size reserves approximate space so the scrollbar thumb and anchor links stay stable.

.chapter {
  content-visibility: auto;
  contain-intrinsic-size: auto 520px; /* width height */
}

The auto keyword in the size tuple means “remember the last rendered size and reuse it on subsequent visits” — after the user scrolls through a section once, future skip/unskip cycles use the measured height. For first visit, supply a reasonable estimate (e.g. median chapter height from analytics) or measure sections at build time.

Underestimating intrinsic height produces minor CLS when sections expand; overestimating leaves blank gaps. Harbor Commerce computed per-section heights at build time from Markdown word counts and embedded a style="contain-intrinsic-size: auto 480px" attribute per chapter, then let the auto keyword refine after first paint.

Harbor Commerce buyer’s guide refactor

The guide shipped as semantic HTML — one <article> with 38 <section id="chapter-N"> blocks. Each section held headings, paragraphs, a spec comparison table, and two product images. No JavaScript framework; static hosting on a CDN.

  1. Added a shared class .cv-section with content-visibility: auto and contain-intrinsic-size: auto 500px.
  2. Excluded the hero summary and sticky table-of-contents nav with content-visibility: visible so LCP and anchor jumps stayed instant.
  3. Precomputed per-section intrinsic heights in the static site generator from word count × average line height plus table row estimates.
  4. Verified find-in-page: searching for a SKU in chapter 31 forced that section to render without scrolling — acceptable UX.
  5. Measured with Lighthouse and WebPageTest: initial main-thread tasks fell from 1.2 s to 340 ms; LCP element (hero image) was unchanged because it sat in the always-visible header block.

The team considered virtual scrolling but rejected it: the page needed full SEO crawlability, deep anchor links, and print styles without a React runtime. Content visibility delivered most of the perf win with a 12-line CSS addition.

Pairing with Intersection Observer and lazy media

Content visibility and Intersection Observer solve different problems and compose well:

  • Content visibility — browser-native render skipping; no JavaScript required; works on static HTML.
  • Intersection Observer — application logic on visibility changes: swap data-src to src, fetch infinite-scroll pages, fire analytics viewability events.

On Harbor’s category grid, cards use content-visibility: auto for layout skip and an observer with rootMargin: 200px to lazy-load images inside each card. The CSS handles structural deferral; the observer handles network deferral. Do not assume content visibility replaces image lazy loading — skipped subtrees may still decode eager src attributes when eventually rendered.

Technique decision table

Approach Best for Trade-off
content-visibility: auto Long static pages, docs, article sections, card grids in SSR HTML Needs intrinsic-size tuning; limited Safari support before 18.0
Virtual scrolling 10k+ homogeneous rows, chat logs, data tables in SPA DOM nodes unmounted — breaks SEO, print, find-in-page unless patched
Intersection Observer lazy load Deferring network fetches (images, iframes, JSON) Does not skip layout of heavy HTML already in the DOM
Code splitting + route lazy load Reducing initial JavaScript parse/execute Orthogonal to render skipping; combine both
Pagination / “load more” Very long lists where even lazy render cost adds up Extra clicks; worse deep-link UX
display: none until expanded Accordions, tabs with tiny hidden bodies Content absent from layout; bad for SEO if abused on main copy

Browser support and progressive enhancement

Chromium and Firefox support content-visibility broadly. Safari added support in 18.0 (2024). Unsupported browsers ignore the property and render normally — safe progressive enhancement with no runtime feature detect required. For Safari 17 and below on critical revenue pages, measure whether intrinsic-size gaps justify a polyfill; usually they do not.

Test print styles: skipped sections should render in print media. Wrap skip rules in @media screen if your print stylesheet needs every chapter:

@media screen {
  .cv-section {
    content-visibility: auto;
    contain-intrinsic-size: auto 500px;
  }
}

Common pitfalls

  • Missing intrinsic size: without contain-intrinsic-size, skipped sections collapse to zero height and cause scroll jank and CLS when they expand.
  • Breaking sticky positioning: content-visibility on an ancestor of a position: sticky element can prevent stickiness. Keep sticky headers outside skipped subtrees.
  • Anchor link jumps: clicking a table-of-contents link to a skipped section forces synchronous render — acceptable but may hitch on very large sections. Pre-render the target section on mouseenter of TOC links if needed.
  • Find-in-page surprises: some browsers render all matches lazily; others require scroll. Test Ctrl+F on long docs.
  • Overlapping layout dependencies: sections whose height depends on siblings (masonry without fixed rows) are poor candidates — use container queries or JS masonry instead.
  • Assuming it lazy-loads images: network requests still fire when the subtree renders unless you also defer src.
  • Accessibility audits: screen readers generally access skipped content, but verify with VoiceOver and NVDA on your section boundaries.

Practitioner checklist

  • Apply content-visibility: auto to independent, repeated sections — not the entire <body>.
  • Always set contain-intrinsic-size with a reasonable height estimate.
  • Keep LCP heroes and sticky nav outside skipped subtrees.
  • Pair with lazy image loading for network wins inside each section.
  • Scope skip rules to @media screen if print needs full content.
  • Test anchor navigation, find-in-page, and browser back/forward cache.
  • Measure LCP, INP, and CLS before and after — content visibility should improve INP, not hurt CLS.
  • Verify SEO: crawlers that execute layout should still index deferred sections.
  • Document section height estimates in your static generator for maintainability.
  • Re-evaluate when Safari share drops — intrinsic-size tuning matters most on first paint.

Key takeaways

  • content-visibility: auto lets the browser skip layout and paint for off-screen subtrees until the user scrolls near them.
  • contain-intrinsic-size reserves approximate height so scrollbars and anchor links stay stable.
  • It complements — does not replace — Intersection Observer lazy loading and JavaScript code splitting.
  • Best on long static HTML: docs, guides, comment threads, and large SSR product grids.
  • Progressive enhancement: unsupported browsers render normally with no breakage.

Related reading