PipelineSPEC-EVOLVINGENGINE-SPECIFICGENERAL

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.

The question

Can the browser simply not render the parts of the page nobody can see, and what does that cost me?

The user intent

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.

The obvious build

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.

Why it breaks

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).

How it breaks in a real browser
  • 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 faster display: 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.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • content-visibility: auto applies 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-size rather 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: hidden skips the contents unconditionally. Unlike display: none it 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

ENGINE-SPECIFICThe "approaching the viewport" step is entirely heuristic — how far ahead an engine renders is unspecified and has changed between versions. Do not design an experience that depends on content being ready before it is visible; treat early rendering as a bonus rather than a contract, because another engine may render later or not at all until the box intersects.

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.

The life of a skipped section
  1. 1
    Far 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.

  2. 2
    Approaching 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.

  3. 3
    Becomes 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.

  4. 4
    Rendered

    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.

  5. 5
    Scrolled 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.

When the guess is wrong
TriggerSymptomCauseResponse
No contain-intrinsic-size suppliedPage scroll height is far too small; the scrollbar is a sliver that grows as you scrollSize containment applies while skipped, so every skipped box measures zeroAlways pair auto with an intrinsic size; treat the missing declaration as a bug, not an optimisation.
A fixed intrinsic size well below real content heightThe scroll thumb shrinks continuously and the content under the finger driftsEach rendered section replaces a small guess with a larger real sizeUse contain-intrinsic-size: auto <length> so the first render is remembered, and set the length from measured content (Visual Stability).
Fast scroll through many sectionsStutter during rapid scrolling that was not there beforeSeveral subtrees become relevant in one frame and are laid out togetherReduce per-section layout cost, or virtualise instead if the list is uniform and long (List Virtualization).
Deep link to a section far down the pageThe page lands slightly above or below the targetThe offset was computed from guessed sizes for everything above itImprove the estimate, and re-scroll after render if the exact position matters (Scroll Restoration).
Applied to individual list rowsNo measurable saving, plus the scrollbar behaviourPer-boundary bookkeeping costs as much as the layout it skips for small elementsApply it to groups, not to leaves; the unit should be something with real layout inside it.
A long document's sections
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 the
7 estimate used before that has ever happened. */
8 contain-intrinsic-size: auto 900px;
9}
10
11/* Preserved state, deliberately unreachable: an inactive tab panel
12 whose scroll position and form state should survive. This is NOT
13 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.

accessibility specA long page whose sections use `content-visibility: auto`, navigated by keyboard, find-in-page and screen readerLong document with skipped sections

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.

TabMoves 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 + FFind-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 / SpaceScrolls 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
  • 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.
Announces
  • 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 auto subtrees 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-expanded on 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.

ApproachRendered when off screenIn the accessibility treeFind-in-pageState preservedCost to reveal
content-visibility: autoNo — skipped while not relevantYesYes — a match makes it relevantYesLayout and paint for the subtree, in the frame it becomes relevant
content-visibility: hiddenNo — always skippedNoNoYes — rendering state is keptCheaper than display: none, because state was preserved
display: noneNot rendered at allNoNoNo — boxes are destroyedFull style and layout for the subtree as if new
hidden attributeNot rendered at allNoNoNoSame as display: none
visibility: hiddenLaid out, not paintedNoNoYesPaint only — the geometry was maintained the whole time
Virtualisation (nodes removed)Nodes do not existNoNoOnly what your code preservesCreate 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 auto with contain-intrinsic-size, and prefer the auto <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 hidden for content that must be findable. If the user should be able to search for it, auto is the value; hidden is 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 prefer auto over hidden (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: none or do not render it. If you need it available to everyone but cheap, use auto. hidden is a narrow third case — preserved state, deliberately out of reach — and picking it by accident is an accessibility regression.

What can go wrong

Failure modes
  • 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.
  • hidden used 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-size with 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.
What can arrive out of order
  • 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: auto arrive 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.
Security
  • 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 hidden to keep privileged content from a user. Anything the client received, the user has (The Browser Security Model).
Misreads
  • "It is like display: none but faster." auto renders content when it becomes relevant and keeps it findable; display: none removes 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.
  • "hidden is the aggressive version of auto." hidden is 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

How you would see this
  • 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 hidden where you meant auto.
  • 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).
Slow device, slow network, large data, old tab
  • 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.
What this costs
  • 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-size is 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-size companion 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.

Computer Architectureworking-settemporal-locality
Domains that do not exist yet
  • 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.