The CSS declaration content-visibility: auto lets the browser skip rendering
an element’s contents until they’re near the viewport or otherwise needed.
Calling it "lazy rendering" might be a helpful analogy.
Content visibility demo
Here’s a demo of a simple landing page with cat pictures:
There’s not much to see; the difference is under the hood. Compare recordings in your browser’s Performance panel with the declaration enabled and disabled to see how much rendering work it saves.
How does "content-visibility: auto" actually work?
Browsers are generally fast at rendering HTML (it’s their 9-to-5 job), but they still break a sweat on sites packed with complex layouts and visual effects.
But why spend time laying out a module at the bottom of the page when you’re at the top? Off-screen content can still affect the rest of the layout, and its descendants might be positioned on-screen. Browsers can render a page progressively, but they can’t simply ignore everything outside the viewport.
content-visibility: auto applies containment to a section, limiting how its
contents affect the surrounding page. This lets the browser skip much of the
section’s rendering work while it’s off-screen. Rendering resumes as the section
approaches the viewport, or when its contents become relevant through focus or
selection, for example.
We’re all more or less familiar with image lazy loading, right? The analogy is
useful, but this defers rendering work, not resource downloads. It doesn’t
replace loading="lazy" on images or stop JavaScript from running.
The contents remain in the DOM and available to in-page search and keyboard navigation. web.dev’s explanation goes into more detail. Early Chrome versions had a screen reader bug, documented in the accessibility issues section below.
The code
Start with two CSS declarations:
.module {
content-visibility: auto;
contain-intrinsic-size: auto 0 auto 500px;
}While the contents are skipped, the browser sizes the element as though it were empty. Without a specified height or a placeholder, an ordinary block can collapse to zero content height.
contain-intrinsic-size
provides that placeholder. Here, auto 0 sets a fallback content width of zero,
and auto 500px sets an estimated content height of 500 pixels. In normal block
layout, the module’s automatic width still fills the available space.
Each auto tells the browser to reuse the last rendered size in that dimension
when available. The original example used 0 500px, without explicitly asking
the browser to remember the size.
These are intrinsic content dimensions, not a fixed outer size: padding and borders still apply, and the contents determine the height when rendered.
The three main values for content-visibility are:
- visible
- The default: the contents are laid out and painted normally.
- hidden
The contents are skipped, including for in-page search and keyboard navigation. Unlike setting
display: noneon the element, its own box still participates in layout. Steve Faulkner’s post "Short note on content-visibility: hidden" explores the early implementation’s accessibility behavior.- auto
Applies layout, style, and paint containment. The browser can skip rendering the contents when they aren’t relevant to the user, adding size containment while they’re skipped. Unlike
hidden, this keeps the contents available to browser features such as in-page search.
Some gotchas
There are a few tradeoffs to keep in mind.
Jumpy scrollbar
As you scroll, the scrollbar can jump or change size as placeholder heights are
replaced with actual heights. The closer contain-intrinsic-size is to the
rendered content size, the smaller the adjustment. Getting the estimate right
can be tricky on responsive sites, where text wrapping changes the height.
The auto values help on subsequent visits to a section by reusing its measured
size, but the first render still relies on your estimate. I’ll take a small
scrollbar adjustment for a useful speedup, though it’s worth checking the result
at different viewport widths.
Containment changes layout behavior
Paint containment clips overflowing descendants, and layout containment changes the containing block for absolutely and fixed-positioned descendants. Check menus, tooltips, and other elements that need to escape a section’s bounds before applying this broadly. These effects follow from the CSS containment rules.
Also, JavaScript that measures skipped descendants can force the browser to do the rendering work you were trying to avoid.
Accessibility issues
The original concern was about headings. Marcy Sutton investigated it:
Neat. But my spidey accessibility senses went off as soon as I read those words: off-screen and rendering. [...] My initial thought was that
content-visibility: autocould pose an access problem on initial load by suppressing part of the page outline from screen readers, so I set out to test it. Marcy Sutton
In Chrome 85–89, headings inside skipped sections could be missing from screen reader navigation, including NVDA’s Elements List. Users had to scroll through the page before the full heading structure became available.
The workaround was to keep headings and landmarks outside sections using
content-visibility: auto. That recommendation addressed the old bug; it is not
a general restriction on using the property today. Marcy’s article now documents
the fix.
There is a separate caveat: skipped descendants that are intentionally hidden
with display: none or visibility: hidden can still appear in the
accessibility tree if their styles haven’t been evaluated. See
MDN’s accessibility notes.
Test with keyboard navigation and screen readers, especially if a section mixes
visible and intentionally hidden content.
Conclusions
content-visibility: auto can reduce rendering work on long pages with complex
sections. Pair it with a reasonable placeholder size, then measure the effect on
your page.
A cool side effect can be smoother window resizing, because there’s less off-screen layout for the browser to calculate.