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.
- Added a shared class
.cv-sectionwithcontent-visibility: autoandcontain-intrinsic-size: auto 500px. - Excluded the hero summary and sticky table-of-contents nav with
content-visibility: visibleso LCP and anchor jumps stayed instant. - Precomputed per-section intrinsic heights in the static site generator from word count × average line height plus table row estimates.
- Verified find-in-page: searching for a SKU in chapter 31 forced that section to render without scrolling — acceptable UX.
- 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-srctosrc, 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-visibilityon an ancestor of aposition: stickyelement 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
mouseenterof 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: autoto independent, repeated sections — not the entire<body>. - Always set
contain-intrinsic-sizewith 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 screenif 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: autolets the browser skip layout and paint for off-screen subtrees until the user scrolls near them.contain-intrinsic-sizereserves 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
- Core Web Vitals explained — LCP, INP, and CLS budgets content visibility should improve
- Intersection Observer API explained — lazy loading and viewability on top of render skip
- Virtual scrolling explained — when DOM recycling beats CSS deferral
- CSS container queries explained — responsive sections inside skipped card grids