content-visibility
Ask the engine to skip rendering work for content nobody is looking at yet — and take on responsibility for its size, its scrollbar and whether anyone can find it.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
Can the browser simply not render the parts of the page nobody can see, and what does that cost me?
A person opens a long page — a documentation site, a feed, a report with fifty sections — and expects it to become usable immediately, not after the browser has laid out content forty screens below.
Put content-visibility: auto on every section. The browser skips the off-screen work and the page gets faster, for one line of CSS and no downside.
The scrollbar becomes a liar. Skipped sections are sized from contain-intrinsic-size, so the scrollbar reflects guesses; as you scroll and real sizes replace the guesses, the thumb jumps and the page under your finger moves (Visual Stability).
- The scrollbar becomes a liar. Skipped sections are sized from
contain-intrinsic-size, so the scrollbar reflects guesses; as you scroll and real sizes replace the guesses, the thumb jumps and the page under your finger moves (Visual Stability). - The work is deferred, not deleted. A section entering the viewport is laid out and painted in that frame, so fast scrolling turns a one-time load cost into a repeated scrolling cost (Scroll and Input Latency).
- Deep links and restored scroll positions have to resolve into content that has never been laid out, which forces the browser to render it synchronously and can land you at the wrong offset (History and Navigation).
content-visibility: hidden— the other value — removes content from find-in-page and from the accessibility tree. Reaching for it as "a fasterdisplay: none" changes what users can discover.- Applied to small elements it is a net loss: the bookkeeping per boundary costs more than the layout it skips, and you get the correctness risk anyway.
What is actually happening
In the browser, not in the framework.
content-visibility: autoapplies containment to the element and, while its contents are not relevant to the user, skips their rendering work entirely — style, layout and paint for the subtree do not run.- While skipped, size containment applies too, so the box is sized from
contain-intrinsic-sizerather than from its contents. That is why supplying an intrinsic size is not optional in practice. - "Relevant to the user" is defined by the engine and includes more than visibility: being in or near the viewport, containing focus, containing the selection, or being a find-in-page match. When an element becomes relevant, containment relaxes and the content renders.
contain-intrinsic-size: auto <length>tells the engine to remember the size the element had the last time it was rendered and to use the length only before that has ever happened — which is what stops the scrollbar drifting on the second pass.content-visibility: hiddenskips the contents unconditionally. Unlikedisplay: noneit preserves rendering state, so revealing it later is cheaper — but the content is not searchable and is not exposed to assistive technology.- None of this affects the network or scripts. Resources referenced by skipped content still load on their own schedule, and script in the subtree runs exactly as before (Lazy Loading).
What this makes the browser do
And which of it is avoidable.
- Tracking relevance per element: proximity to the viewport, focus, selection and find-in-page matches, updated as the user scrolls.
- Skipping style, layout and paint for skipped subtrees, which is the entire point and the whole saving.
- Rendering a subtree at the moment it becomes relevant, inside a frame that also has to handle the scroll that made it relevant.
- Maintaining remembered sizes for
contain-intrinsic-size: auto, and reconciling the scroll offset when a guessed size is replaced by a real one. - Avoidable work removed: laying out and painting content forty screens down that the user may never reach. Work added: relevance bookkeeping, and layout at scroll time instead of at load time.
What `auto` skips, and when it stops skipping
The feature is best understood as containment with a switch on it. While the engine decides your subtree is not relevant to the user, it applies size containment on top of the usual layout, style and paint containment, and simply does not do the work. When relevance flips, the containment relaxes and the content renders like anything else.
The important part is that relevance is broader than "on screen". Focus, selection and find-in-page all count, which is what keeps the feature honest: content is skipped for the purposes of rendering, not withdrawn from the user. That is the difference between this and every "just do not render it" approach that has broken search on a long page.
- 1Far off screen
Contents are skipped: no style, no layout, no paint for the subtree. The box is sized from
contain-intrinsic-size.fails by No intrinsic size supplied, so the box collapses and the whole page reports the wrong scroll height.
- 2Approaching the viewport
The engine decides the subtree is about to be relevant and begins rendering it, ideally before the user can see it.
fails by Fast scrolling outruns the heuristic, so the user reaches content that has not been laid out yet.
- 3Becomes relevant
Containment relaxes; style, layout and paint run for the subtree in this frame. Focus, selection and find-in-page trigger this as well as visibility.
fails by Several sections becoming relevant at once put all of their layout into one frame, which drops it.
- 4Rendered
Behaves like ordinary content. With
contain-intrinsic-size: auto, the real size is remembered for later.fails by The real size differs a lot from the guess, so the scrollbar and the scroll offset are corrected under the user.
- 5Scrolled away
Becomes irrelevant again and is skipped, now with a remembered size rather than a guess.
fails by Content whose size depends on state that changed while it was skipped renders differently on return.
Nothing here touches the network or script: resources in a skipped subtree load on their own schedule, and script runs normally.
You now own the size
The moment you skip a subtree's layout, the browser can no longer know how tall it is — and it needs a height to build a scrollbar and to place everything below it. contain-intrinsic-size is where that number comes from, and it is now your responsibility.
A wrong number is not a rendering error. It is a scrollbar whose thumb changes size as you drag it, a page whose total height shifts as you read, and a fragment link that lands near but not on its target. The auto keyword exists to make this self-correcting after the first render, which turns a permanent problem into a first-pass one.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
No contain-intrinsic-size supplied | Page scroll height is far too small; the scrollbar is a sliver that grows as you scroll | Size containment applies while skipped, so every skipped box measures zero | Always pair auto with an intrinsic size; treat the missing declaration as a bug, not an optimisation. |
| A fixed intrinsic size well below real content height | The scroll thumb shrinks continuously and the content under the finger drifts | Each rendered section replaces a small guess with a larger real size | Use contain-intrinsic-size: auto <length> so the first render is remembered, and set the length from measured content (Visual Stability). |
| Fast scroll through many sections | Stutter during rapid scrolling that was not there before | Several subtrees become relevant in one frame and are laid out together | Reduce per-section layout cost, or virtualise instead if the list is uniform and long (List Virtualization). |
| Deep link to a section far down the page | The page lands slightly above or below the target | The offset was computed from guessed sizes for everything above it | Improve the estimate, and re-scroll after render if the exact position matters (Scroll Restoration). |
| Applied to individual list rows | No measurable saving, plus the scrollbar behaviour | Per-boundary bookkeeping costs as much as the layout it skips for small elements | Apply it to groups, not to leaves; the unit should be something with real layout inside it. |
1/* Skip work for sections the reader has not reached. */2.doc-section {3 content-visibility: auto;4 5 /* Required in practice: the size the engine uses while skipping.6 `auto` remembers the real size once rendered; 900px is the7 estimate used before that has ever happened. */8 contain-intrinsic-size: auto 900px;9}10 11/* Preserved state, deliberately unreachable: an inactive tab panel12 whose scroll position and form state should survive. This is NOT13 findable by find-in-page and NOT exposed to assistive technology. */14[role="tabpanel"][hidden-by-app] {15 content-visibility: hidden;16}Pick the estimate from real content, not from a round number — and if your sections vary by an order of magnitude in length, that variance is the scrollbar behaviour your users will experience.
Find-in-page, focus and the accessibility tree
This is the section that decides whether the feature is safe to ship. auto keeps content discoverable: the browser treats a find-in-page match, a focus move or a selection as making the subtree relevant, so the user finds what they are looking for and the page renders it on demand. hidden does the opposite, deliberately.
The distinction is not a footnote about screen readers. Find-in-page is how a very large number of people navigate long documents, and a section that cannot be found is missing as far as they are concerned. Choosing hidden because it sounded more thorough is one of the more damaging one-word mistakes available in CSS.
semantics Each section is a landmark or a heading-led region — section with an accessible name, or a heading that the section follows. Containment changes rendering, never roles: the structure a screen reader navigates must be the same with and without it.
| Tab | Moves focus into a skipped subtree, which makes it relevant and renders it. Focus must end up visible, not on a box that is still being laid out. |
| Ctrl/Cmd + F | Find-in-page matches inside skipped content with auto, and does not match at all with hidden — the single most important behavioural difference between the two values. |
| Page Down / Space | Scrolls into skipped content; the render must keep up, or the user sees a blank region for a frame. |
| Heading navigation (screen reader) | Jumps between section headings. With auto the headings are exposed; with hidden the whole subtree is absent from the tree. |
- — Focus entering a skipped subtree must trigger rendering and end with the focused element visible and its focus ring unclipped.
- — Focus order must not change based on whether a section happens to be rendered — it is a document-order property, and skipping must not reorder anything.
- — After a fragment navigation, focus should move to the target section, not just the scroll position, so keyboard and screen-reader users land where sighted users do.
- — Nothing should be announced when a section is skipped or rendered — this is a rendering optimisation, not a state change, and announcing it would be noise.
- — Headings and landmarks inside
autosubtrees must remain announceable during document navigation, since that is how many users move through a long page. - — If your application also hides sections as a feature — collapsed accordions — that state belongs in
aria-expandedon the control, independently of any containment (Live Regions and Announcement).
usually broken by Reaching for content-visibility: hidden because it sounds like the stronger optimisation. It removes the content from find-in-page and from the accessibility tree, so the page keeps its scrollbar and its structure while becoming unsearchable — and the bug is reported as "search is broken", which nobody traces back to a CSS property.
| Approach | Rendered when off screen | In the accessibility tree | Find-in-page | State preserved | Cost to reveal |
|---|---|---|---|---|---|
content-visibility: auto | No — skipped while not relevant | Yes | Yes — a match makes it relevant | Yes | Layout and paint for the subtree, in the frame it becomes relevant |
content-visibility: hidden | No — always skipped | No | No | Yes — rendering state is kept | Cheaper than display: none, because state was preserved |
display: none | Not rendered at all | No | No | No — boxes are destroyed | Full style and layout for the subtree as if new |
hidden attribute | Not rendered at all | No | No | No | Same as display: none |
visibility: hidden | Laid out, not painted | No | No | Yes | Paint only — the geometry was maintained the whole time |
| Virtualisation (nodes removed) | Nodes do not exist | No | No | Only what your code preserves | Create nodes, then style and layout (List Virtualization) |
How to build it
Most important first.
- Apply it to large, self-contained, off-screen chunks: sections of a long document, groups in a long list, cards in a feed. The saving scales with the work skipped, so a boundary is only worth it if there is real layout behind it.
- Always pair
autowithcontain-intrinsic-size, and prefer theauto <length>form so the first real measurement is remembered. - Make the fallback length a decent estimate of typical content. A wrong guess is not a correctness bug but it is a scrollbar that jumps, which users read as the page being broken.
- Never use
hiddenfor content that must be findable. If the user should be able to search for it,autois the value;hiddenis for content you are deliberately taking out of reach, like an inactive tab panel whose state you want to preserve. - Consider whether virtualisation is the better tool. For a list of ten thousand rows, removing the DOM nodes beats keeping them and skipping their rendering, because the nodes themselves cost memory and style (List Virtualization).
- Verify with the keyboard, with find-in-page and with a screen reader before shipping. This is a feature whose failure mode is content nobody can reach (Accessibility Testing).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- With
auto, skipped content stays in the accessibility tree and remains findable — the browser makes it relevant when it is searched for, focused, or selected. This is the whole reason to preferautooverhidden(The Accessibility Tree). - With
hidden, content is not exposed to assistive technology and not matched by find-in-page. Using it as a performance optimisation on real content makes that content unreachable for screen-reader users while remaining "on the page" for everyone else. - Tabbing into a skipped subtree makes it relevant, so keyboard navigation works — but the frame in which that happens does the deferred layout, so focus can arrive perceptibly late on a slow device (Keyboard Operability).
- A skipped section that is announced as present but whose layout has not run can report a size that does not match what a magnifier user is about to see, which shows up as the viewport jumping after focus lands (Focus Management).
- If you need content hidden from everyone, use
display: noneor do not render it. If you need it available to everyone but cheap, useauto.hiddenis a narrow third case — preserved state, deliberately out of reach — and picking it by accident is an accessibility regression.
What can go wrong
- Scrollbar jitter: intrinsic sizes that differ substantially from reality, so the scroll thumb resizes continuously as the user scrolls.
- Scroll anchoring surprises: the position under the user's finger moves when a guessed size is replaced by a real one mid-scroll.
- Jank on fast scroll: several sections become relevant in a single frame and are all laid out at once, so the page stutters exactly during rapid movement.
- Fragment navigation landing in the wrong place, because the target had no layout when the offset was computed.
hiddenused for performance on content users then cannot find with the browser's own search — a bug reported as "the search does not work", never as a CSS problem.- The mitigation failing: adding
contain-intrinsic-sizewith a fixed length that is right for the average section and badly wrong for the two longest ones, which are the ones people scroll to.
- Relevance changes are driven by scrolling, which the compositor may be running ahead of the main thread — so an element can be visible for a frame or two before its content has been rendered.
- Remembered sizes from
contain-intrinsic-size: autoarrive as content is rendered for the first time, so the same page scrolled twice behaves differently the second time. - A find-in-page match, a focus move and a scroll can all make different subtrees relevant in the same frame, producing a layout burst that no single interaction accounts for.
- Fragment navigation on a freshly loaded page races the rendering of the target section: the offset is computed against sizes that are still guesses.
- Skipping rendering does not skip anything else. Content in a skipped subtree is in the DOM, readable by any script on the page, and present in the page source — it is not hidden in any sense that matters to an attacker (What the Frontend Is Responsible For in Auth).
- Resources referenced by skipped content still load, so this is not a way to avoid fetching something; use lazy loading for that, and remember it is a hint the browser may ignore.
- Relevance is observable: an element becoming relevant is something script can detect, which makes "did the user scroll to this" a measurable signal — fine for analytics you disclose, and a tracking surface you should treat as one (Session Replay and the Privacy It Costs).
- Do not use
hiddento keep privileged content from a user. Anything the client received, the user has (The Browser Security Model).
- "It is like
display: nonebut faster."autorenders content when it becomes relevant and keeps it findable;display: noneremoves it from the tree entirely. They are not substitutes. - "It reduces the amount of DOM." It reduces the rendering work for existing DOM. Every node is still there, still costing memory, still matched by queries (Detached Nodes and What Keeps Them Alive).
- "It stops off-screen images from loading." It does not; loading is a separate concern with separate mechanisms.
- "
hiddenis the aggressive version ofauto."hiddenis a different feature: unconditional skipping, no find-in-page, no accessibility exposure, preserved state. Choosing it for speed changes behaviour users depend on. - "If the page still scrolls smoothly, the intrinsic size does not matter." The intrinsic size decides the scrollbar and the scroll offset. A wrong guess is a stability problem even when frame rate is perfect.
Measuring it, and what changes in the field
- Compare style and layout time on initial load with and without it. The saving should be visible as a smaller layout event at load, and if it is not, the content below the fold was cheap and the boundary is not earning anything (Debugging Rendering and Jank).
- Record a scroll and look for layout events during it. That is the deferred cost arriving, and it tells you whether you traded a load problem for a scrolling one.
- Watch layout shift during scroll: intrinsic-size guessing shows up directly in visual stability measurements, not in load timing (Visual Stability).
- Test find-in-page explicitly. It is the fastest check that you have not used
hiddenwhere you meantauto. - In the field, look at loading and interaction signals together — this optimisation moves work between them, so improving one while quietly degrading the other is the expected failure (Vitals in the Field).
- The benefit scales with how much content is off screen and how expensive it is: fifty sections of rich content, yes; a page that fits in two viewports, no.
- On a slow device both sides of the trade grow — the load saving is larger and the render-on-scroll cost is more likely to drop frames.
- With variable-height content the intrinsic-size guess is harder and the scrollbar behaves worse, which is why uniform cards suit this better than prose sections of wildly different lengths.
- On a page restored from the back-forward cache or with a restored scroll position, the browser has to resolve an offset into content that may never have been laid out in this session.
- Support and relevance heuristics differ by engine and by version, so a page tuned against one browser can behave measurably differently in another — feature-detect rather than assume, exactly as with any other recent platform feature.
- You are trading load-time layout for scroll-time layout. That is a good trade when the user reads a little of a long page and a bad one when they scroll through all of it quickly.
- You take on responsibility for sizes the browser used to compute.
contain-intrinsic-sizeis a number you maintain, and content changes can invalidate it without anyone noticing. - It is one declaration, which makes it easy to apply broadly and hard to attribute later. Codebases accumulate it in places nobody measured, along with its scrollbar behaviour.
- Compared with virtualisation it is far simpler and keeps the whole document in the DOM — which means find-in-page and deep links keep working, and memory keeps scaling with content (List Virtualization).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- SPEC-EVOLVINGThis is a comparatively recent addition to CSS: the property, the
contain-intrinsic-sizecompanion and the definition of "relevant to the user" have all been refined, and engines shipped them at different times. Feature-detect and verify behaviour in the browsers you support rather than treating any specific behaviour here as settled. - ENGINE-SPECIFICHow far outside the viewport an engine starts rendering skipped content, and how it reconciles scroll offsets when a guessed size is replaced, are implementation choices rather than specified ones. Blink shipped this first and its heuristics are the ones most tuning targets; another engine can produce noticeably different scrolling behaviour on identical CSS.
- GENERALThe trade itself — deferring rendering work in exchange for owning the size and accepting work at scroll time — is inherent to the feature, not to any implementation, and holds wherever it is supported.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — an automated check that content remains findable and focusable with containment applied, which is the regression this feature actually produces and which no visual diff catches.