helping-browsers-optimize-with-the-css-contain-property

About The Author

Rachel Andrew is not only Editor in Chief of Smashing Magazine, but also a web developer, writer and speaker. She is the author of a number of books, including …
More about
Rachel
Andrew

The CSS contain property gives you a way to explain your layout to the browser, so performance optimizations can be made. However, it does come with some side effects in terms of your layout.

In this article, I’m going to introduce a CSS Specification that has just become a W3C Recommendation. The CSS Containment Specification defines a single property, contain, and it can help you to explain to the browser which parts of your layout are independent and will not need recalculating if some other part of the layout changes.

While this property exists for performance optimization reasons, it can also affect the layout of your page. Therefore, in this article, I’ll explain the different types of containment you can benefit from, but also the things you need to watch out for if applying contain to elements in your site.

The Problem Of Layout Recalculation

If you are building straightforward web pages that do not dynamically add or change elements after they have loaded using JavaScript, you don’t need to worry about the problem that CSS Containment solves. The browser only needs to calculate your layout once, as the page is loaded.

Where Containment becomes useful is when you want to add elements to your page without the user needing to reload it. In my example, I created a big list of events. If you click the button, the first event is modified, a floated element is added, and the text is changed:

A listing of items with a button to change some of the content in the first item
(See the initial example on CodePen)

When the content of our box is changed, the browser has to consider that any of the elements may have changed. Browsers are in general pretty good at dealing with this, as it’s a common thing to happen. That said, as the developer, you will know if each of the components is independent, and that a change to one doesn’t affect the others, so it would be nice if you could let the browser know this via your CSS. This is what containment and the CSS contain property gives you.

How Does Containment Help?

An HTML document is a tree structure which you can see when inspecting any element with DevTools. In my example above, I identify one item that I want to change by using JavaScript, and then make some changes to the internals. (This means that I’m only changing things inside the subtree for that list item.)

DevTools with the list item of the featured item expanded to see the elements inside
Inspecting a list item in DevTools

Applying the contain property to an element tells the browser that changes are scoped to the subtree of that element, so that the browser can do any possible optimizations — safe in the knowledge that nothing else outside of that element will change. Exactly what a particular browser might do is down to the engine. The CSS property simply gives you — as the developer and expert on this layout — the chance to let it know.

In many cases, you will be safe to go right ahead and start using the contain property, however, the different values come with some potential side effects which are worth understanding before adding the property to elements in your site.

Using Containment

The contain property can set three different types of containment:

  • layout
  • paint
  • size

Note: There is a style value in the Level 2 Specification. It was removed from Level 1, so does not appear in the Recommendation, and is not implemented in Firefox.

Layout

Layout containment brings the biggest benefits. To turn on layout containment, use the following snippet:

.item {
  contain: layout;
}

With layout containment enabled, the browser knows that nothing outside the element can affect the internal layout, and nothing from inside the element can change anything about the layout of things outside it. This means that it can make any possible optimizations for this scenario.

A few additional things happen when layout containment is enabled. These are all things which ensure that this box and contents are independent of the rest of the tree.

The box establishes an independent formatting context. This ensures that the content of the box stays in the box — in particular floats will be contained and margins will not collapse through the box. This is the same behavior that we switch on when we use display: flow-root as in explained in my article “Understanding CSS Layout And The Block Formatting Context”. If a float could poke out of your box, causing following text to flow around the float, that would be a situation where the element was changing the layout of things outside it, making it a poor candidate for containment.

The containing box acts as the containing block for any absolutely or fixed position descendants. This means it will act as if you had used position: relative on the box you have applied contain: layout.

The box also creates a stacking context. Therefore z-index will work on this element, it’s children will be stacked based on this new context.

If we look at the example, this time with contain: layout, you can see that when the floated element is introduced it no longer pokes out the bottom of the box. This is our new Block Formatting Context in action, containing the float.

A listing of items, a floated element is contained inside the bounds of the parent box
Using contain: layout the float is contained (See the layout containment example on CodePen)

Paint

To turn on paint containment, use the following:

.item {
  contain: paint;
}

With paint containment enabled, the same side effects as seen with layout containment occur: The containing box becoming an independent formatting context, a containing block for positioned elements, and establishing a stacking context.

What paint containment does is indicate to the browser that elements inside the containing block will not be visible outside of the bounds of that box. The content will essentially be clipped to the box.

We can see this happen with a simple example. Even if we give our card a height, the floated item still pokes out the bottom of the box, due to the fact that the float is taken out of flow.

A floated box poking out the bottom of a containing box
The float is not contained by the list item

With paint containment turned on the floated item is now clipped to the size of the box. Nothing can be painted outside of the bounds of the element with contain: paint applied.

A box with a floated box inside which has been cut off where it escapes the box
The content of the box is clipped to the height of the box (See the paint example on CodePen)

Size

Size containment is the value that is most likely to cause you a problem if you aren’t fully aware of how it works. To apply size containment, use:

.item {
  contain: size;
}

If you use size containment then you are telling the browser that you know the size of the box and it is not going to change. This does mean that if you have a box which is auto-sized in the block dimension, it will be treated as if the content has no size, therefore the box will collapse down as if it had no contents.

In the example below, I have not given the li a height; they also have contain: size applied. You can see that all of the items have collapsed as if they had no content at all, making for a very peculiar looking listing!

A listing of items with a button to change some of the content in the first item
(See the size example on CodePen)

If you give the boxes a height then the height will be respected when contain: size is used. Alone, size containment will not create a new formatting context and therefore does not contain floats and margins as layout and paint containment will do. It’s less likely that you would use it alone; instead, it is most likely you would apply it along with other values of contain to be able to get the most possible containment.

Shorthand Values

In most cases, you can use one of two shorthand values to get the best out of containment. To turn on layout and paint containment, use contain: content;, and to turn on all possible containment (keeping in mind that items which do not have a size will then collapse), use contain: strict.

The Specification says:

contain: content is reasonably “safe” to apply widely; its effects are fairly minor in practice, and most content won’t run afoul of its restrictions. However, because it doesn’t apply size containment, the element can still respond to the size of its contents, which can cause layout-invalidation to percolate further up the tree than desired. Use contain: strict when possible, to gain as much containment as you can.”

Therefore, if you do not know the size of the items in advance, and understand the fact that floats and margins will be contained, use contain: content. If you do know the size of items in addition to being happy about the other side effects of containment, use contain: strict. The rest is down to the browser, you have done your bit by explaining how your layout works.

Can I Use Containment Now?

The CSS Containment specification is now a W3C Recommendation which is what we sometimes refer to as a web standard. In order for the spec to get to this stage, there needed to be two implementations of the feature which we can see in both Firefox and Chrome:

Screenshot of the browser support information on Containment on Can I Use
Browser support for containment (Source: Can I Use)

As this property is transparent to the user, it is completely safe to add to any site even if you have lots of visitors in browsers that do not support it. If the browser doesn’t support containment then the visitor gets the experience they usually get, those in supporting browsers get the enhanced performance.

I would suggest that this is a great thing to add to any components you create in a component or pattern library, if you are working in this way it is likely each component is designed to be an independent thing that does not affect other elements on the page, making contain: content a useful addition.

Therefore, if you have a page which is adding content to the DOM after load, I would recommend giving it a try — if you get any interesting results let me know in the comments!

The following resources will give you some more detail about the implementation of containment and potential performance benefits:

Smashing Editorial(il)

two-browsers-walked-into-a-scrollbar

Mona Lisa Overflow (image provided by Scott Jehl)

The scrollbar is a humble but productive mechanism that operates as the primary means through which one can traverse a document. But that’s not all a scrollbar can do! This modest workhorse also provides a meaningful hint at how long the document is, pulling double duty as a document progress bar too.

The scrollbar is under attack. Scrolljacking hijacks the default scrolling behavior, breaking the implied contract between document length and scrollbar height.

Moreover, touch devices have popularized hiding the scrollbar, making it invisible until an overflowing element is scrolled, trading design aesthetics for confusion on containers that don’t appear to be overflowing/scrollable at all.

Classical desktop operating systems have continued this trend, attempting to minimize the design intrusiveness of the classic scrollbar.

Before we get too far, let’s get a few definitions out of the way:

  • Obtrusive scrollbars: scrollbars that take up screen real estate. These do not overlay on top of the content, but appear next to it.
  • Unobtrusive scrollbars: scrollbars that sit on top of the content. These don’t subtract screen real estate away from the container they belong to.

Current Behavior

By default on both iOS and Android scrollbars are unobtrusive.

On Mac OS (Mojave at time of writing), scrollbars are hidden until the element is scrolled. This is the default behavior when a mouse is not connected to the machine. There are three options for this in the General pane in your System Preferences:

Mac OS General System Preferences pane with Automatically based on mouse or trackpad selected

This preference was confirmed to control how scrollbars behave in Chrome, Firefox, and Safari, and new Chromium-based Edge.

Watch the following video to see how the Mac OS user preference changes the obtrusiveness of the scrollbar:

On Windows 10, a similar preference exists in Settings → Display → Simplify and personalize Windows.

Windows 10 preference pane with Automatically hide scroll bars in Windows checked

Unfortunately even with this preference checked, it had no effect on scrollbar behavior in Firefox, Chrome, in Internet Explorer and Edge—whether Chromium or EdgeHTML based.

The Problem

Demo of the default scrollbar style on Windows 10 (in Chrome)

Windows scrollbars are not only obtrusive by default but are particularly heavy, design-wise. They’re much wider by default than their Mac OS counterparts and typically conform to operating system colors (not a page’s color palette).

For designers accustomed to Mac environments but designing multi-platform web experiences, trying to make everyone happy in a way that doesn’t place a lot of performance burden on the end user can be tricky.

Our Requirements

  1. We want desktop scrollbars to be more visually appealing. Especially important for overflow containers inside of the viewport that need to blend better with the visual design aesthetic (in my opinion, visuals are not not quite as important for page-level scrollbars, but that’s a point of contention I’m sure).
  • Minimize the real estate that a scrollbar can occupy. Windows scrollbars are obtrusive and very wide by default.
  • Respect changes to user preferences. If a user has selected non-default options for scrollbar behavior, respect those preferences when possible.
  • Avoid JavaScript heavy solutions that normalize unobtrusive scrollbars and place a performance burden on the end user (e.g. the lovely OverlayScrollbars plugin).

How far can we get with CSS?

<div class="overflowing-element">div>
.overflowing-element {

overflow-y: auto;

-webkit-overflow-scrolling: touch;

-ms-overflow-style: -ms-autohiding-scrollbar;

}

If you fancy unobtrusive scrollbars on Internet Explorer and EdgeHTML based Edge, use -ms-overflow-style: -ms-autohiding-scrollbar; and they will magically swap (easy, right?).

Side note that when iOS 13 is released, -webkit-overflow-scrolling: touch may not be required for improved scrolling physics on iOS (although you may want to keep it around for a bit for older iOS versions).

You may also want to read up on the related CSS property overscroll-behavior, which controls how the document scrolls when the overflow container content scrolls to the boundary.

Firefox

Firefox supports the unprefixed CSS properties scrollbar-color and scrollbar-width.

Note that for clarity these code examples use CSS variables, which are not supported by Internet Explorer 11.

:root {

--scrollbar-track-color: transparent;

--scrollbar-color: rgba(0,0,0,.2);



--scrollbar-width: thin;

}

.overflowing-element {

scrollbar-width: var(--scrollbar-width);

scrollbar-color: var(--scrollbar-color) var(--scrollbar-track-color);

}

Chrome/Safari/Chromium-Edge/et al

WebKit/Blink-based browsers support non-standard pseudo-elements for customization.

:root {

--scrollbar-track-color: transparent;

--scrollbar-color: rgba(0,0,0,.2);



--scrollbar-size: .375rem;

--scrollbar-minlength: 1.5rem;

}

.overflowing-element::-webkit-scrollbar {

height: var(--scrollbar-size);

width: var(--scrollbar-size);

}

.overflowing-element::-webkit-scrollbar-track {

background-color: var(--scrollbar-track-color);

}

.overflowing-element::-webkit-scrollbar-thumb {

background-color: var(--scrollbar-color);



}

.overflowing-element::-webkit-scrollbar-thumb:vertical {

min-height: var(--scrollbar-minlength);

}

.overflowing-element::-webkit-scrollbar-thumb:horizontal {

min-width: var(--scrollbar-minlength);

}

However, there is a minor issue with this code. When you set a height or width on the ::-webkit-scrollbar pseudo-element, on Mac OS it will swap unobtrusive scrollbars to be obtrusive (overriding the default configuration). However, we can fix this with a little JavaScript!

CSS and a tiny bit-o-JavaScript

We can add a tiny little JavaScript feature test to detect if the default scrollbar is obtrusive or not. It looks something like this:



* Scrollbar Width Test

* Adds `layout-scrollbar-obtrusive` class to body

* if scrollbars use up screen real estate.

*/


var parent = document.createElement("div");

parent.setAttribute("style", "width:30px;height:30px;");

parent.classList.add('scrollbar-test');



var child = document.createElement("div");

child.setAttribute("style", "width:100%;height:40px");

parent.appendChild(child);

document.body.appendChild(parent);







var scrollbarWidth = 30 - parent.firstChild.clientWidth;

if(scrollbarWidth) {

document.body.classList.add("layout-scrollbar-obtrusive");

}



document.body.removeChild(parent);

We apply our layout-scrollbar-obtrusive class to the document when our scrollbars are obtrusive. We can use this to only apply width and height to scrollbars that are obtrusive, avoiding the swapping behavior described previously (and respecting user preferences!).

.layout-scrollbar-obtrusive .layout-scrollbar::-webkit-scrollbar {

height: var(--scrollbar-size);

width: var(--scrollbar-size);

}

How did we do?

On touch devices with unobtrusive scrollbars (e.g. iOS and Android), we keep the default behavior for free.

On Mac OS, we are able to respect the user’s system preferences. That means no unintended swapping between unobtrusive and obtrusive scrollbars. We only apply our style to obtrusive, visible scrollbars to meet our design requirements.

On Windows, in Firefox and Chrome there was no option for unobtrusive scrollbars but we were able to apply our CSS-only control here as well. With working demos of CSS customized scrollbars in place, we were able to get buy-in from the design team and settle on this middle ground, avoiding a heavier JavaScript solution.

Review the Demos


Mona Lisa image created by Scott Jehl

All blog posts