CSS “contain” property
TL;DR The contain
CSS property allows you to define an element as a style boundary in order to optimize the browser’s paints, layouts, composite, and style contexts calculations.
.el {
contain: strict;
}
The contain
CSS property is somewhat obscure but well supported by Chrome and Firefox and has some powerful performance potential behind it. The W3C recommendation specs just landed on November 21, 2019.
When you make any changes to your DOM dynamically, either through CSS events (:hover
, animation
, …) or through JavaScript, or even by adding or removing DOM nodes, the browser has to recalculate all or parts of the page to determine how the layout has to reflow, what layers it has to repaint, how to composite the result, and how CSS-generated content might be affected. This can often become a very costly operation and is a good place to look at for fluidity optimizations. And many CSS properties do cause this!
To demonstrate the different possibilities, let’s set up a basic DOM and stylesheet, and have a basic animation:
<div class='parent'>
<div class='child'>regular</div>
</div>
<div class='parent containment'>
<div class='child'>contained</div>
</div>
.parent {
background-color: lightslategray;
}
.child {
box-shadow: inset 0 0 0 1px black;
margin: 10px;
height: 20px;
width: 80px;
position: relative;
left: 0;
animation: anim 1s linear infinite alternate;
}
@keyframes anim {
from { left: 0; }
to { left: 120px; }
}
Limiting repaints:
The simplest of the values to understand is paint
: nothing outside the contain: paint
box can be painted. If the box itself if outside of the viewport, it won’t be painted at all. This allows the browser to skip many paint steps!
.containment {
contain: paint;
}
To observe the repaints in Chrome, in the developer tools, click ⋮ > More tools > Rendering and check Paint flashing.
Simplifying layout calculations:
Next up is size
: nothing inside the contain: size
box can change its dimensions. This is a powerful improvement because it greatly simplifies what needs to be computed during the layout phase: you don’t need to look at the children to know the parent’s size.
.containment {
contain: size;
}
@keyframes anim {
from { width: 80px; }
to { width: 200px; }
}
Simplifying CSS scopes side-effects:
The most obscure of the available values is style
. It prevents style side-effects from reaching outside of the contain: style
box. However, up bubbling effects are contrary to the cascading principle, and thus rarely used. This property was actually dropped from the latest W3C recommendation.
Here’s an example anyway:
.containment {
contain: style;
}
.parent {
counter-reset: i;
}
.parent::after, .child::after {
content: counter(i);
counter-increment: i;
}
Limiting reflows:
This last property, layout
, is probably the most powerful in terms of optimizing reflow calculations. It forces the contain: layout
box to become an independent BFC (block formatting context). A BFC is like a mini layout. Floated elements are contained within the BFC. Margins don’t collapse with the outside of the BFC. absolute
and fixed
positionning are calculated in reference to the BFC. It creates its own stacking context…
.containment {
contain: layout;
}
@keyframes anim {
from { margin-top: 0; }
to { margin-top: 20px; }
}
In this example, you can see that in the regular box, the child has its margin “leaking” outside of the parent (this is because of margin collapse), whereas since the contain: layout
box creates a BFC, the margin of the child is calculated from within the parent.