---
layout: post
title: "What Is CSS Containment and How Can I Use It?"
date: 2026-04-02 11:30:00
categories: Web Development
main: ""
meta: "CSS containment lets you isolate layout and paint work to self-contained ‘islands’. Here’s what each contain value does and how to use it safely."
faq:
- question: "What is CSS containment?"
answer: "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."
- question: "What does the contain property do?"
answer: "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."
- question: "When should I use CSS containment?"
answer: "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."
- question: "What is the difference between contain and content-visibility?"
answer: "contain constrains browser work around a component, while content-visibility can go further by skipping work for off-screen content until it is needed."
- question: "Can CSS containment cause bugs?"
answer: "Yes. It can introduce clipping, sizing surprises, new stacking contexts, and other side effects, so it should be applied deliberately and tested carefully."
---
Continuing my work on _web performance for design systems_, I want to look at
a woefully underused CSS feature called
[containment](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/contain).
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!
{% include promo.html %}
## Why Containment Exists
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:
1. recalculate styles for affected elements,
2. recalculate layout (sizes and positions),
3. repaint the pixels that changed, and
4. re-composite layers on screen.
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:
* changes inside it will not influence layout or paint outside it, and
* in some cases, its own size does not depend on its children.
Once you make that promise, the browser can exploit it. For example:
* it can avoid relayout of the rest of the page when something changes inside
a contained widget, or
* it can skip painting descendants of an off-screen container entirely.
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_](/2023/07/core-web-vitals-for-search-engine-optimisation/)).
## Meet the `contain` Property
The main lever you’ll pull is the `contain` property:
```css
.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:
* `layout`
* `paint`
* `size`
* `style`
You can specify them individually or in combinations:
```css
.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.
{% include cross-sell.html %}
## Layout Containment
`contain: layout` tells the browser that the internal layout of the
element is completely independent of the rest of the page.
```css
.card {
contain: layout;
}
```
In practice, that means:
* layout calculations for descendants of `.card` do not affect layout
outside of `.card`,
* the `.card` establishes its own formatting context (like a mini layout world),
* floats and margin-collapsing are contained within `.card`,
* `.card` becomes the containing block for `position: absolute` and
`position: fixed` descendants, and
* a new stacking context is created, so `z-index` on children is scoped
to that card.
This has two main consequences:
1. **Performance.** When something changes inside `.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.
2. **Layout behaviour.** The new formatting context and containing block can be
very handy for controlling floats, margin-collapsing, and `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:
```css
.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**
* A fixed-position child that used to be anchored to the viewport will
now be anchored to the contained element instead. If you are using
`position: fixed` for tooltips or overlays, check them carefully.
* Margin-collapsing rules change; that is usually a feature, but it can
affect legacy layouts that rely on collapsing from the first child
into the parent.
## Paint Containment
`contain: paint` isolates painting to the element’s box:
```css
.card {
contain: paint;
}
```
With paint containment in place:
* nothing inside `.card` can visibly overflow its padding box,
* if the `.card` itself is off-screen, the browser does not need to paint its
children at all, and
* you still get a stacking context and containing block, as with layout
containment.
This is ideal for widgets where everything should stay visually inside
the box anyway: cards, tiles, thumbnail previews, and so on.
**Gotchas**
* Any intentional overflow effects (drop-shadows that extend beyond the box,
popovers that stick out of a sidebar, decorative ‘bleeds’) simply will not
show. If you depend on overflow, do not use `contain: paint` on that element.
* If you combine `layout` and `paint` containment, you will see both behaviours:
independent layout plus clipped painting.
## Size Containment
`contain: size` is more specialised. It decouples an element’s _size_ from its
contents:
```css
.widget {
contain: size;
}
```
When you turn on size containment:
* the size of `.widget` is calculated as if it had no children, and
* children can still be laid out and painted inside, but they do not inform the
container’s own size.
This 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`:
```css
.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**
* Forgetting to set a size often leads to zero-height containers, which then
hide their children or cause unexpected scroll behaviour.
* The size containment itself does not buy you much performance on its own; it
is mainly there to unlock other optimisations safely.
## Style Containment
`contain: style` is probably the least glamorous of the four, but it has
a specific job:
```css
.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:
* Counters created or modified inside `.section` will not affect counters
outside it.
If you have ever used [automatic numbering for headings or
figures](https://github.com/csswizardry/csswizardry.github.com/blob/d537a2fb16f9c1bd0644d3446487677a6de97ece/css/_elements.type.scss#L172)
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`.
## Special Values: `content` and `strict`
Typing `contain: layout paint style` over and over gets boring _fast_. The spec
therefore gives us a couple of convenient shorthands.
### `contain: content`
```css
.article {
contain: content;
}
```
`content` is shorthand for:
```css
.article {
contain: layout paint style;
}
```
You get:
* independent internal layout,
* clipped painting within the padding box, and
* counters and similar style side-effects scoped to the subtree.
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:
* blog posts on a listing page,
* cards in a grid,
* rows in a data table,
* slides in a carousel…
…as long as nothing needs to extend outside of the bounds of them (e.g. tooltips
in said data table).
### `contain: strict`
```css
.panel {
contain: strict;
contain-intrinsic-size: 600px 400px;
}
```
`strict` is shorthand for:
```css
.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:
* you need to give the panel an explicit or intrinsic size,
* you need to be confident that nothing inside the panel is meant to
overflow visually, and
* you need to understand that `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.
{% include promo.html %}
## Containment in the Real World
So far we have stayed fairly abstract. Let us start with a real example before
we look at a few more generic patterns.
### OpenTable’s Mobile Drawer
A couple of years ago, I ran [a custom INP workshop](/workshops/) for the
wonderful [OpenTable](https://www.opentable.com/) (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 `contain`ment, 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)
why is this tooltip behind that overlay?, check for containment on ancestor elements as part of your debugging checklist. ### Interaction with Container Queries 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: * If you are already using container queries, you may get some of the benefits of containment ‘for free’ on those containers. * If you add more explicit containment on the same elements without thinking about it, you might over-constrain things, particularly with `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. ## When to Use Containment 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. ### Good Candidates You should *strongly* consider `contain: content` (or similar) for: * self-contained cards or tiles arranged in grids or lists; * repeated article previews on index pages; * sections of a long, scrolling page that are visually and functionally independent; and * third-party widgets that should not bleed into the rest of the layout. You should consider `content-visibility: auto` plus `contain-intrinsic-size` for: * very long lists of similar items where many are off-screen; * pages with dozens of article previews or product tiles; and * heavy, mostly static sections further down a page. ### Use With Care 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). ### Probably Avoid Containment usually does **not** belong on: * root- or page-level wrappers where everything inside is interdependent; * small inline elements like buttons or labels; or * elements whose whole purpose is to extend beyond their own bounds visually. {% include cross-sell.html %} ## Closing Thoughts 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](/2022/08/measure-what-you-impact-not-what-you-influence/), 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.