content-visibility

Avatar of Idorenyin Udoh
Idorenyin Udoh on (Updated on )

Take your JavaScript to the next level at Frontend Masters.

The content-visibility property in CSS indicates to the browser whether or not an element’s contents should be rendered at initial load time. So, as the browser starts loading content and is playing it on the screen, this property allows us to step in and tell the browser not to load the contents of an element until it’s needed. Think of it sort of like lazy loading in the sense that an off-screen element’s children are not rendered until they enter the viewport.

.element {
  content-visibility: hidden;
}

The main point of using content-visibility is performance. It can help to speed up page load because the browser is able to defer rendering elements that are not in the user’s viewport until the user scrolls to them. The results can be dramatic. Here are the results of a performance test that Una Kravets and Vladimir Levin put together showing the difference that content-visibility can make on a typical webpage.

Source: web.dev

Syntax

content-visibility: [visible | auto | hidden];
  • Initial value: visible
  • Applies to: elements for which layout containment can apply
  • Inherited: no
  • Computed value: as specified
  • Animation type: not animatable

Values

The content-visibility property accepts one of three values:

  • hidden: The element bypasses its contents (kind of similar to applying display: none; to the contents).
  • visible: There is no effect and the element is rendered as normal.
  • auto: The element has layout, style, and paint containment. The browser gets to determine if this content is relevant to the user and, if it isn’t, then the browser will skip it. At the same time, the element is still focusable, selectable and accessible to things like tabbing and in-page search.

content-visibility: hidden;

I mentioned above it’s sort of similar to display: none; because the element is not painted to the page at all.

When the rule is removed, the browser has to render the element and its contents. With content-visibility: hidden;, however, the element is hidden, but its rendered state is cached. So when the rule is removed, the browser does not have to render the element from scratch. So you might use this on something that is hidden by default, but there is a high chance that you’ll show it at some point in the lifecycle of the page (e.g. a commonly used modal). That way, the element loads much faster on subsequent visits when it does render.

The note in the spec is clear on the accessibility of the hidden value:

The skipped contents must not be accessible to user-agent features, such as find-in-page, tab-order navigation, etc., nor be selectable or focusable.

content-visibility: auto;

You could think of this sort of like lazy loading for entire parts of the DOM.

If an element is below the fold, and has the content-visibility: auto; rule, the browser does not consider any of its contents. Hence, rendering for that element is skipped. As the user scrolls to the element, the browser paints its contents and the rendering is done soon enough for the user. The heuristics on when the browser decides to render are unclear and likely left up to the browser to decide.

Accessibility concerns

Careful when using content-visibility: hidden. That not only tells the browser to skip an element from rendering on initial page load, but also removes the element from being read by assistive technology, like screen readers.

Conversely, the auto keyword tells the browser that an element’s content isn’t needed on initial page load as long as it’s off screen. In other words, that element is skipped, just like it is when we use the hidden keyword.

However, auto also indicated that the element should still be available to the user rather than completely ignoring the element in the DOM. What this means is that the element and its content should be focusable, selectable, tabbable, and searchable via the browser’s find-in-page feature. Again, the spec is super clear on that point:

Unlike hidden, the skipped contents must still be available as normal to user-agent features such as find-in-page, tab order navigation, etc., and must be focusable and selectable as normal.

In addition, off-screen elements that have been hidden with CSS (e.g. display: none;), should have aria-hidden="true" applied to them since the browser has not rendered them. This way, they are still present in the accessibility tree.

UX concerns

You can imagine if large chunks of a web page are not rendered, the length of a scrollbar might indicate the page has much less content on it than it actually does. Alex Russell talks about (and has) solutions for this in his “content-visiblity Without Jittery Scrollbars” post.

Performance

Again, this property is all about buying performance. The results can be significant. See this video with Jake Archibald and Surma where they implement it and see big changes:

The contain-intrinsic-size property

Naturally, when a browser renders all of a webpage’s content initially, it knows the height that’s needed to display everything (based on the aggregate of the individual heights of all the page’s sections) and scrolling will be seamless. However, content-visibility: auto;, when applied to an element, treats the element as though it has an height of 0 (assuming the height property was not explicitly set). When the user scrolls and the element comes into view (and the browser starts to paint it), the actual height is calculated and this causes a layout shift on the page. Scrolling might make the webpage a little janky too — something known as Cumulative Layout Shift (CLS) which is considered a big deal for search engine optimization, as Google now includes it in its Core Web Vitals metrics for measuring the performance of a site.

The solution to big layout shifts is to pair content-visibility: auto; with contain-intrinsic-size, giving the browser a predictable height to use as a placeholder for the element when it paints.

.off-screen-parent {
  content-visibility: auto;
  contain-intrinsic-size: 4800px; /* A guess at the height of it */
}

contain-intrinsic-size acts as a placeholder for the not-yet-rendered content. Thereby curbing the scroll and layout shift issues. So if you know the exact height, you can use that as the value. If not, make an estimate. When the section is being rendered, if the height you set is not the actual height, then there will be a little layout shift but it will not be as pronounced as without the contain-intrinsic-size property.

Demo

I created a basic website with dummy text and a total of 185 images.

A screenshot of the site

I used GitHub Pages for the hosting. One branch has the content-visibility and contain-intrinsic-size properties and the other does not. So, I compared the both:

The results without content-visibility applied to the element.
The page with content-visibility rendered in about a quarter of the time.

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
85NoNo85No

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
94No94No

More information