Newly available in Baseline 2024 is the content-visibility CSS property and as its name implies, it controls the visibility of an element’s content.
What makes this property different than selecting all of an element’s children and setting a rule for them like display: none
or visibility: hidden
is that content-visibility applies to all of an element’s content — including text nodes and pseudo-content — not just child elements.
In the following demo, the visibility of the content is different due to the use of display: none
versus content-visibility:hidden
. Validate what's happening by inspecting with browser dev tools.
Comparison of using content-visibility
.display-none > * {
display: none;
}
.content-hidden {
content-visibility: hidden;
}
This element is hidden.
This is the only visible text because its a text node inside of display: noneThis element is hidden.
This text is also hidden.It’s also different than having display: none
or visibility: hidden
directly applied to the element itself, since for those properties the entire box isn’t visible, while for content-visibility
its own box is generated and visible.
But there’s more to content-visibility
than just being a visibility on-off switch — it allows browsers to fully skip rendering for the contents. This means skipping style calculation, layout, and paint. The property is a power tool in a web author’s performance kit. Before we unwrap the syntax of content-visibility
, we need to talk about CSS containment.
CSS Containment
#If you’ve used container queries, then you’ve already encountered containment, whether you were aware of it or not.
Containment isolates a subtree of the document to improve performance and stability. This also allows features like container queries to exist since it helps avoid infinite loops.
There are four types of containment:
- Size containment ignores the element’s contents when determining its size.
- Layout containment prevents contents inside and outside from affecting each other’s layout. For example,
margin
on content will not collapse with the parent’s margin (and therefore surrounding elements). Other features like baseline alignment do not affect an element with its layout contained. - Paint containment clips any content from within the element that is protruding outside and isolates the content, which affects properties like
mix-blend mode
. - Style containment affects features like CSS counters: contained content cannot affect counters outside of the container.
Containments can be explicitly applied to an element using the contain
property. container
and content-visibility
both automatically apply the containments they need for their intended purpose.
Containment allows browsers to skip rendering an element’s contents when it’s hidden, which can improve performance, since the browser is free to focus on what’s relevant to the user. Browsers can also save rendering work, since the isolation created by containment makes rendering more predictable, improving the speed at which content can be hidden or made visible.
Now that we have a cursory understanding of containment let’s dig into content-visibility
.
Using content-visibility
#There are three values for content-visibility
: visible
, hidden
, and auto. We’ll explore how each works, but we’ll end up focusing on auto
as it’s extremely useful.
The initial value: visible
#The initial value of the property is visible
. When this is applied, no containments are applied to the element, and it has no performance optimizations. This is the regular old element you’re familiar with.
Manually hiding content with hidden
#content-visibility: hidden
hides the element’s contents and applies a strong set of containments to the element: size, layout, paint, and style. It’s very important to note that when content-visibility
is set to hidden
that content will be hidden from the accessibility tree and “find-in-page” browser features. Later we’ll touch on a couple of emerging web platform features that use content-visibility: hidden
and differ a small bit in this regard.
In this example, we’ve got the same box with content using both the visible
and hidden
values of content-visibility
. In the latter case, the box is still generated, but the content takes up no space.
While the contents are hidden, it’s possible to measure them. This will trigger the browser’s rendering process for the contents, but the content will remain hidden.
Letting the browser decide using auto
#When the auto
value is used, the browser hides the element’s contents while it’s not relevant to the user. This can serve as an easy performance win for many use cases.
Whether the contents are skipped for rendering or not, a set of containments is applied to the element:
- When the contents are rendered: layout, paint, and style containment are used on its containing element.
- When the contents are skipped: layout, paint, style, and
size
containment are used on its containing element.
An important difference between hidden
and auto
is that auto
keeps the content in the accessibility tree and ensures it’s still findable with a browser’s “find-in-page” feature.
Pairing with contain-intrinsic-size
#When content-visibility
is set to hidden
or auto
, and the element’s content is skipped, size containment is applied to the element. This means that the size of the element will be purely based on whatever dimensions are set for itself, its own padding, border-size, or the layout that it’s a part of. In many cases that’s not an issue, however, for some it can create problems. This is where the contain-intrinsic-size
property and its longhands can help.
Long, overflowing lists are a great use case for content-visibility: auto
, but that on its own can create some issues, as the scrollbar no longer becomes a reliable indicator of scroll progress.
In this example, we don’t have an intrinsic block-size for the element, so when it’s hidden the content take up no space which causes the scrollbar to be larger than what it would be if all of the list items are rendered. As the list items render, notice that the scrollbar changes sizes, getting smaller as it renders more items.
If the block-size of the list items is consistent, we can set contain-intrinsic-block-size
to whatever the size of the content is.
li {
content-visibility: auto;
contain-intrinsic-block-size: 2lh;
}
In this example, the content of each list item is stable, so we can set contain-intrinsic-block-size
to the block-size
of the content. Notice that the scroll remains stable and doesn’t jitter.
In cases where there is no consistent size, but you have a good idea of what the average is, you can set auto
before that value, and this will cause the property to remember what its size was if it ever was rendered. Before then, it’ll use the other value as a fallback.
li {
content-visibility: auto;
contain-intrinsic-block-size: auto 2lh;
}
In this example, we don’t have a stable item size, so instead we’ve taken the average which we’ll use with contain-intrinsic-block-size
as a fallback. We’ve set auto
as the preferred which uses the last known rendered size as its value. Again, notice that the scrollbar mostly remains stable without jittering.
When content-visibility
is used on elements within a complex layout, it can accidentally trigger undesirable layout reflow, when the content becomes visible again, and the size containment is dropped. This is another case where setting the intrinsic size of the contained element will help.
What makes content relevant?
#content-visibility: auto
doesn’t just render content when it’s on-screen; it’s a bit more nuanced than that: it renders content when it is considered relevant to the user. The following are conditions that make content relevant for the user:
- The content is close to the viewport.
- The content has an element with focus.
- The content’s text is selected.
- The content is in the top layer (i.e. the
<dialog>
element and Popover API both use the “top layer” to keep their content on top of everything else). - The content is part of a view transition.
Observing whether content is skipped with JavaScript
#You can listen for the contentvisibilityautostatechange
event — yes, it’s a doozy to spell out. The event has a skipped
property which indicates when the content is currently skipped. While this is helpful for being able to confirm that the feature is indeed working, you can also use it to either start up or tear down expensive features depending on the skipped state (e.g. rendering on a <canvas>
element or make network requests).
This is the same example we used for using an average intrinsic block-size for the elements, but I’ve included some JavaScript to log out whether elements are skipped using the contentvisibilityautostatechange
event, so make sure to view the console as you scroll the items.
What does content-visibility not do?
#While the auto
or hidden
value can skip rendering the content, this does not prevent resources such as images from downloading eagerly, so it’s a good idea to employ some sort of lazy-loading strategy alongside content-visibility
. That could look like using the loading=lazy
attribute.
How does the platform use content-visibility?
#There are a few emerging features in the web platform that use content-visibility
.
::details-content
#One is the ::details-content pseudo-element. This is a part of the <details>
element’s shadow tree — yes, many HTML elements are implemented with the Shadow DOM. ::details-content
is a part-like pseudo-element that exposes the internal container element. In Chromium that container element is <slot>
which defaults to display: contents
, ::details-content
sets the element as display: block
and then hides the content with content-visibility: hidden
. This means you can both style the element that houses the content and more easily animate it.
As of writing, this feature is only available in Chromium-based browsers (versions 131 and later — see Can I use…
for ::details-contents
current availability).
hidden=until-found
#The other emerging feature is the hidden=until-found
attribute and value combination, which hides an element's contents until it’s found using the “find-in-page” feature. Instead of setting display: none
for the element, the until-found value sets content-visibility: hidden
.
As of writing, this feature is only available in Chromium-based browsers (versions 102 and later — see Can I use…
for hidden=until-found
current availability).
Differences with content-visibility: hidden
#One important note for both of these features that is unlike how content-visibility: hidden
works by both hiding the content from the accessibility tree and “find-in-page” features, both the <details>
element and hidden=until-found
allow their content to be findable. Once found, the <details>
element expands, and the hidden
attribute is removed.
Summary
#Let’s cover what we’ve learned:
content-visibility
makes it easy to hide content, completely skip rendering, and to opt-in to greater performance when we find ourselves in situations filled with content.- Having a grasp of CSS containment is important when using
content-visibility
because it causes different types of containments to be applied to the element. - We can use another feature of the CSS containment suite,
contain-intrinsic-size
and its longhands, to create an intrinsic size for elements with their contents skipped to avoid strange scrollbars or unexpected layout reflows.
Further reading
#If you’re interested in learning more, I recommend checking out the following resources. Some of the included blog posts have great examples of content-visibility
being used in practice with lots of visual demonstrations and deep dives into the performance.
- CSS Containment Module Level 2 (spec)
- content-visibility on MDN
- “content-visibility: the new CSS property that boosts your rendering performance” on web.dev
- “Improving rendering performance with CSS content-visibility” by Nolan Lawson
- “Using CSS content-visibility to boost your rendering performance” on the LogRocket blog
Nathan selected Indigenous Friends Association for an honorary donation of $50
Indigenous Friends Association is dedicated to cultivating digital pathways grounded in Indigenous ways of knowing and being. They create tech opportunities and bridge the digital divide for Indigenous communities.