Need Some Help?
I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.




























Written by on CSS Wizardry.
Independent writing is brought to you via my wonderful Supporters.
contain Propertycontent and strict
contain: contentcontain: strictcontent-visibility and contain-intrinsic-size
content-visibilityContinuing my work on web performance for design systems, I want to look at a woefully underused CSS feature called containment. I fear it’s underused because there isn’t much written about it, so this piece aims to be the post I wish I’d been able to read a few years ago. Hopefully it demystifies things a little for you, too.
Modern UIs are busy places. A single page might contain a fixed header, an infinite scroll feed, a sticky cart summary, and a few different third-party widgets stuck around the edges. Every time something changes, the browser needs to decide how much of that page it has to re-think: styles, layout, painting, compositing, and so on.
Out of the box, the browser needs to be cautious. If it cannot be sure that a change is local, it behaves as if almost anything might have moved, forcing it to touch more of the page than is strictly necessary. While this is safe, it’s not always cheap.
CSS containment is a way of telling the browser where the boundaries really are.
You use it to say this bit of the DOM is independent of the rest; you can
treat it as a self-contained island
. In return, the browser is free to skip
work outside those islands when things change.
In this post, we’ll take a look at what CSS containment actually is, what each
contain value does, and how to use it confidently on real projects without
creating hard-to-debug side effects. We’ll also look at my most favourite
real-world example of just how effective containment can be. Let’s go!
Need Some Help?
I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.
To understand containment, you don’t need to memorise the entire rendering pipeline, but you do need a rough mental model of where the work goes.
When something changes in the DOM, a browser typically has to:
The expensive part is often how far those changes spread. A small change in one part of the tree can invalidate layout or painting in other parts—or even the whole document—and the browser cannot always know that the effect is local. That is why relatively simple operations on a large application can feel sluggish: the browser is having to think about the whole world.
Containment is a promise that a given subtree is isolated:
Once you make that promise, the browser can exploit it. For example:
All of that reduces work on the main thread, which is exactly what we want when we are trying to keep interactions snappy (think Interaction to Next Paint).
contain PropertyThe main lever you’ll pull is the contain property:
.card {
contain: content;
}
At a high level, contain lets you assert different kinds of independence for
an element and its subtree. There are four basic types of containment:
layoutpaintsizestyleYou can specify them individually or in combinations:
.card {
contain: layout paint;
}
On top of those, there are a couple of shorthand values:
content: shorthand for layout paint style.strict: shorthand for size layout paint style.There is also inline-size, which is a more targeted form of size
containment—we don’t really need to give it much more attention than that.
Let’s go through each of these in turn.
contain: layout tells the browser that the internal layout of the
element is completely independent of the rest of the page.
.card {
contain: layout;
}
In practice, that means:
.card do not affect layout
outside of .card,.card establishes its own formatting context (like a mini layout world),.card,.card becomes the containing block for position: absolute and
position: fixed descendants, andz-index on children is scoped
to that card.This has two main consequences:
.card, the browser can keep
most layout work local to that subtree because it has been told none of those
changes influence other boxes outside.position: fixed
children, but it can also change behaviour compared to an uncontained
element.A simple, realistic use case is a grid of dashboard cards:
.dashboard-card {
contain: layout;
}
Each card may have its own internal structure (headings, charts, small controls), but nothing inside one card affects the layout of any other. When a chart animates inside a card, the browser does not have to re-evaluate layout for the entire dashboard.
Gotchas
position: fixed for tooltips or overlays, check them carefully.contain: paint isolates painting to the element’s box:
.card {
contain: paint;
}
With paint containment in place:
.card can visibly overflow its padding box,.card itself is off-screen, the browser does not need to paint its
children at all, andThis is ideal for widgets where everything should stay visually inside the box anyway: cards, tiles, thumbnail previews, and so on.
Gotchas
contain: paint on that element.layout and paint containment, you will see both behaviours:
independent layout plus clipped painting.contain: size is more specialised. It decouples an element’s size from its
contents:
.widget {
contain: size;
}
When you turn on size containment:
.widget is calculated as if it had no children, andThis is a powerful constraint. Used wrongly, it is also an easy way to create a bunch of zero-sized boxes.
In almost all cases, when you use contain: size you must also provide an
explicit size for the element, either via the usual width/height properties
or via contain-intrinsic-size:
.widget {
contain: size;
contain-intrinsic-size: 400px 300px; /* Fallback/reserved size */
}
The intrinsic size tells the browser what size to assume for layout purposes if
it cannot derive that size from the children. This becomes particularly
important when you start deferring rendering with content-visibility, which
we’ll look at shortly.
Gotchas
contain: style is probably the least glamorous of the four, but it has
a specific job:
.section {
contain: style;
}
Style containment stops certain style-related side effects from spreading outside the contained subtree. The main one you will encounter is CSS counters:
.section will not affect counters
outside it.If you have ever used automatic numbering for headings or
figures
across a page, you will know that counters are global by default. Style
containment is a way of saying this component’s counters are its own, do not
let them leak out
.
It does not give you general purpose ‘scoped CSS’ in the sense of preventing
selectors from matching, so it should not be confused with Shadow DOM or
@scope.
content and strictTyping contain: layout paint style over and over gets boring fast. The spec
therefore gives us a couple of convenient shorthands.
contain: content.article {
contain: content;
}
content is shorthand for:
.article {
contain: layout paint style;
}
You get:
You do not get size containment. That omission is deliberate: without
size, the risk of creating zero-sized boxes is much lower, which makes
contain: content safe to apply quite widely.
This value is an excellent default for self-contained units like:
…as long as nothing needs to extend outside of the bounds of them (e.g. tooltips in said data table).
contain: strict.panel {
contain: strict;
contain-intrinsic-size: 600px 400px;
}
strict is shorthand for:
.panel {
contain: size layout paint style;
}
This is the most aggressive containment you can ask for: the panel’s size no longer depends on its children, its internal layout is independent, its paint is clipped, and its counters are local.
That combination is very powerful, but it comes with responsibilities:
position: fixed children will now anchor to the
panel, not the viewport.As a rule of thumb: do not reach for strict until you have a clear reason and
a good understanding of what will happen.
Need Some Help?
I help companies find and fix site-speed issues. Performance audits, training, consultancy, and more.
So far we have stayed fairly abstract. Let us start with a real example before we look at a few more generic patterns.
A couple of years ago, I ran a custom INP workshop for the
wonderful OpenTable (honestly, web-perf and grub,
can you think of a better client?!). When we think about INP, we usually think
of JavaScript, but the Presentation Delay phase touches a lot of what I call
purple time: recalc style and layout. And while the headline savings below
aren’t going to win me any awards, I do think this is a beautifully succinct and
comprehensive demonstration of contain in action…
On OpenTable’s mobile UI, there’s a language switcher in their off-screen drawer
menu. Tapping the switcher opens a dropdown. The dropdown opens and pushes
content beneath it down the viewport, but it doesn’t affect anything outside
of the drawer menu itself. Without containment, opening the language switcher
scopes a layout event to the whole document, touching 4,371 nodes and applying
new layout to just 41 of them—fewer than 10%:
#document, touching 4,371 nodes to relayout just 41. View full size/quality (1MB)While the whole operation only took 11 milliseconds, that’s still a lot of waste, and we don’t like waste.
By applying contain: strict to the drawer menu’s root DOM node, we can limit
the reach of the work and create incredible savings:
Now, we can see as clear as day that the operation only touched 73 DOM nodes, 40 of which needed attention, and the scope of the work is very clearly the drawer itself. Hyper precise, and about six times faster.
I think this is one of the most beautiful demos I have ever been fortunate enough to present: such a story told in one before/after.
If this kind of rendering and interaction work looks familiar, it is exactly the sort of thing I cover in workshops, masterclasses, and hands-on consultancy.
Once you start looking for them, you will see these opportunities everywhere:
In those kinds of interface, contain: content is often a very good starting
point:
.dashboard-card,
.feed-item {
contain: content; /* layout + paint + style */
}
And where you need to fence off an embed or ad slot more explicitly, this is often enough:
.ad-slot {
contain: layout paint;
}
If those widgets live on other origins, it is also worth configuring your preconnections properly.
content-visibility and contain-intrinsic-sizeWhile this post is about contain, it would be unwise not to mention two
closely related features: content-visibility and contain-intrinsic-size.
content-visibilitycontent-visibility allows the browser to skip layout and painting work
for elements that are not currently relevant to the user, typically
because they are off-screen.
The value you will see most often is auto:
.article-preview {
content-visibility: auto;
contain-intrinsic-size: 600px 400px;
}
With this in place:
contain-intrinsic-size value tells the browser what size to
assume for layout purposes before it has actually rendered the
contents.This is particularly effective for long and repetitive pages or lists. You get the performance benefits of virtualisation without having to wire up a full virtualised list implementation, as long as you can provide a reasonable size estimate.
In fact, I use content-visibility on this page you’re reading right now:
.s-post > h2:nth-of-type(2) ~ p {
content-visibility: auto;
contain-intrinsic-size: 1px 250px;
}
This rather unwieldy selector targets every paragraph after the second <h2> on
the page, renders them lazily with content-visibility, and sets their
placeholder size to one pixel wide and 250 pixels tall. These numbers are
somewhat magic and the 250 is derived from ‘roughly how big the average
paragraph on a page is’—nothing more complicated than that.
Because the DOM nodes still exist, off-screen content with
content-visibility: auto can usually still be:
However:
content-visibility: hidden is more like a performance-friendly
visibility: hidden plus containment. The content is not rendered and
may be removed from the accessibility tree, depending on the browser.getBoundingClientRect,
getComputedStyle, etc.) on a skipped subtree may force the browser
to render it early, eating into your performance gains. Keeping such
calls away from skipped content is important.Your mileage may vary.
Containment is not free. You are trading global flexibility for local isolation, and that trade can surprise you in a few ways.
The most common error with size containment (directly or via
content-visibility) is forgetting to provide an explicit or intrinsic
size. The browser then treats the element as if it had no children when
computing its size, and in many layouts that means it collapses to
zero.
Symptoms include:
The cure is simple: always pair contain: size or content-visibility: auto
with contain-intrinsic-size or appropriate width/height constraints.
contain: paint (directly or via content/strict) will clip painting to the
padding box. That is often exactly what you want, but it will break patterns
that rely on overflow:
box-shadow ‘bleeds’ beyond the card boundary, orIn those cases, either move the overflowing element out of the contained subtree or avoid paint containment on that particular container.
Each layout or paint containment boundary establishes a stacking context and a containing block. This is usually a benefit, but can add up:
z-index debugging becomes more involved because more elements define their
own stacking contexts,fixed-position children may suddenly anchor to a widget instead of the
viewport, andtransform/opacity-driven stacking contexts can become
tricky.If you ever find yourself thinking why is this tooltip behind that
overlay?
, check for containment on ancestor elements as part of your
debugging checklist.
Container queries rely on certain forms of containment under the hood; for
example, container-type: inline-size effectively applies layout and style
containment to define a query container.
That means:
size or paint.A sensible approach is to treat query containers as places where you carefully review which kinds of containment are already applied before adding more.
By this point you should have a decent mental map of what the different containment values do. The remaining question is when to actually apply them.
Here is a pragmatic checklist.
You should strongly consider contain: content (or similar) for:
You should consider content-visibility: auto plus
contain-intrinsic-size for:
Be cautious with:
contain: strict: use it only when you have explicitly sized
elements and no desired overflow.contain: size or inline-size: always provide intrinsic or
explicit sizes, and always test in a range of layouts.paint containment around components that intentionally overflow
(tooltips, dropdowns, overlays).Containment usually does not belong on:
CSS containment is a feature that can yield massive benefit when well deployed. It lets the browser do less work and gives you better control over where you spend your purple time.
You don’t need to apply it everywhere—and it might even be a micro optimisation for your use-case—but a small handful of well-placed rules can make measurable improvements to the interactivity of your UI.
Start small, measure carefully, and keep an eye on the gotchas, but before you know it, you’ll have a finely tuned interface that leaves as small a footprint as possible.
Hopefully that’s lifted containment from an obscurity to an everyday part of your arsenal.
CSS containment is a way to isolate parts of the page so the browser can limit how far layout, paint, size, or style calculations need to spread.
The contain property tells the browser which kinds of work a component can keep to itself, using values such as layout, paint, size, and style.
It works best on self-contained UI islands such as drawers, widgets, panels, or components whose internal changes should not affect the rest of the page.
contain constrains browser work around a component, while content-visibility can go further by skipping work for off-screen content until it is needed.
Yes. It can introduce clipping, sizing surprises, new stacking contexts, and other side effects, so it should be applied deliberately and tested carefully.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。