Dec 17

CSS content-visibility

Hide all an element's content, including text nodes and pseudo content.

By Nathan Knowler

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 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:

  1. Size containment ignores the element’s contents when determining its size.
  2. 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.
  3. Paint containment clips any content from within the element that is protruding outside and isolates the content, which affects properties like mix-blend mode.
  4. 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-visibilityvisiblehidden, 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.

See the CodePen.

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:

  1. When the contents are rendered: layout, paint, and style containment are used on its containing element.
  2. 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.

See the CodePen.

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.

See the CodePen.

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.

See the CodePen.

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:

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.

See the CodePen.

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:

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.

Nathan Knowler

Nathan Knowler

Nathan is passionate about making the web for everyone. He’s interested in accessibility, progressive enhancement, and web components. When he’s not deep diving web specs or code spelunking, he can be found adventuring with his family in their frozen home of Winnipeg.

Nathan selected Indigenous Friends Association for an honorary donation of $50

Indigenous Friends Association

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.