Skip to main
Article
The top front of a bright yellow shipping container with the door open and a blue sky behind it

Can We Query the Root Container?

The complexities of containment, overflow, and ‘propagation’

I spoke about Container Queries at both Smashing Conference (San Francisco) and CSS Day (Amsterdam) – where I recommended setting up a root container to replace most media queries. Since then, Temani Afif pointed out a few issues with that approach, and sent me down a rabbit hole of overlapping specs and browser bugs.

Article contents
TL;DR Update :

While there’s a workaround (below) that will allow you to query the root element, it’s not a perfect solution. We’ve found it simpler to apply containment on all the major layout blocks directly inside the body – things like nav, header, main, and footer.

That ensures every nested component will have a container to query, without creating problems on root. It also allows you to put some things intentionally outside a container, if you need them to use the viewport as a positioning context (with position: fixed, for example).

We were told for years that container queries would be impossible to implement – and for good reason. CSS layout depends on a powerful and complicated balancing of ‘content’ and ‘context’:

Introducing ‘container queries’ directly in this system would cause a sort of Observer’s Paradox. We want to measure the context so that we can make changes to the content – but the context size is based on the content size, and so we end up changing the thing we are trying to measure.

The solution is containment, which allows us to break the outward flow of content information, breaking the loop. In order for container size queries to work, we have to contain the size, style, and layout of our containers. We can do that by specifying a container-type of size (to contain both width and height) or inline-size (to only contain the inline axis). We can only query the dimension(s) we contain. Both container types also get layout and style containment.

But containment comes with a price, and can have some surprising side effects:

See the Pen Containment Examples by @miriamsuzanne on CodePen.

While there’s no problem generally with defining lots of containers on a page, we need to be somewhat cautious about the impacts of containment. As a general rule, we want to use inline-size for most containers, and only use size for containers that will be allowed to overflow. When we allow overflow on elements, we generally also give them a way to size based on something other than their contents. The difference between the ‘intrinsic’ (content) size and the ‘extrinsic’ size is what causes overflow in the first place.

Container size queries are mostly intended to solve the problem of nested containers. If we have a .card element which might be in the main body of the page, might be in a narrow sidebar, and might be in a responsive grid – measuring the ‘viewport’ with @media doesn’t tell us much about the space that’s available for each card. With @container, we can have each .card query the space that it lives in.

But container queries provide a few additional features that are not possible with @media, and might be useful even when we’re measuring the outermost container. Most importantly:

The side-effects of containment also seem like they should be minimal on the root element. The root (html) element seems like a great candidate for size containment:

With container-type: size on the root, we could replace most or all @media size queries with @container across our sites. That feels right to me!

👍🏼 It should be.
👎🏼 But it isn’t.
🤷🏻‍♀️ Unless you’re careful?

Currently, adding a container-type of size to the root (html) element will make scrolling impossible:

See the Pen HTML containment & overflow (2d) by @miriamsuzanne on CodePen.

And using inline-size instead will fix the scrollbar issue, but now fixed elements scroll off the page:

See the Pen HTML containment & overflow (1d) by @miriamsuzanne on CodePen.

Neither behavior will work as a default for most sites.

Various people in that thread pointed to ‘root/body propagation’ as the cause. And they’re right – but as far as I can tell, they shouldn’t be. I was involved with several conversations in the CSS Working Group around containment and propagation, and this is not what we decided. It’s also not defined to work like this in the specification.

As far as I can tell, this is a browser bug – implemented in all browsers – that needs to be fixed.

For us as web authors, the root of a document is the html element. But for browsers, there’s more context to worry about – more root than the root – such as the viewport, the document canvas, the initial containing block, and the initial layout block.

I am not an expert on the complexities of these browser concepts – but roughly, together, they describe the context immediately surrounding our web pages in the browser: the ‘viewport’ through which we’re looking, the ‘canvas’ our site is painted onto, and the ‘blocks’ that defines our initial positioning and layout context. For simplicity, I’m going to refer to all of this collectively as The Viewport.

We don’t have direct access to The Viewport. There’s no CSS syntax we can use to select and style any aspect of The Viewport. And yet, we style them all the time using the somewhat quirky and esoteric magic of ‘propagation’ – where styles on one element (in this case html or body) bubble outward (‘propagate’) and apply to a parent element instead.

For example, when we set a background on html, that background propagates to the document canvas instead. If we don’t set a background on html, but we do set a background on body, then the body background propagates instead – skipping over the root, and straight up to the canvas!

See the Pen Body background propagation by @miriamsuzanne on CodePen.

The papayawhip background covers the entire window, despite the body outline surrounding a fraction of that space.

Background isn’t the only element that propagates out from the html or body element to the viewport. I haven’t found a full list anywhere – this is defined spec-by-spec for individual properties – but the one causing issues for us is overflow.

Overflow Viewport Propagation is defined in CSS Overflow Module Level 3:

UAs must apply the overflow-* values set on the root element to the viewport when the root element’s display value is not none.

Unless we hide the root element, overflow properties will always propagate. But not always from the root:

However, when the root element is an HTML <html> element (including XML syntax for HTML) whose overflow value is visible (in both axes), and that element has as a child a <body> element whose display value is also not none, user agents must instead apply the overflow-* values of the first such child element to the viewport. The element from which the value is propagated must then have a used overflow value of visible.

If both the html and body elements are un-hidden (the default), and the html element has the default overflow (visible), then we propagate the overflow from body instead of html. Once that propagation happens, the browser ignores the actual overflow values on both body and html – using a value of visible instead.

This is very similar to how background propagation works, except for one final twist:

If visible is applied to the viewport, it must be interpreted as auto.

So, in the default case – before we apply any outside CSS – the visible default of the root html element is ignored, the visible default of the body element propagates up to the viewport, which ignores the propagated value, treating it as auto instead.

By default, after all that, we get scrollbars when content overflows the viewport. Simple. 🥴

There’s a reason the CSS Working Group has resolved that:

RESOLVED: No future properties should propagate from <body> to the ICB

(‘ICB’ is the Initial Containing Block.)

RESOLVED: deprecate any existing use of body propagation

Body propagation was a mistake. We’re stuck with it now, but we don’t have to protect it moving forward.

CSS Containment Module Level 2 does provide some caveats around root/body propagation and containment:

When any containments are active on either the HTML <html> or <body> elements, propagation of properties from the <body> element to the initial containing block, the viewport, or the canvas background, is disabled. Notably, this affects:

  • writing-mode, direction, and text-orientation (see CSS Writing Modes 3 § 8 The Principal Writing Mode)
  • overflow and its longhands (see CSS Overflow 3 § 3.3 Overflow Viewport Propagation)
  • background and its longhands (see CSS Backgrounds 3 § 2.11.2 The Canvas Background and the HTML <body> Element)

(Maybe this is the complete list?! If you know of a better list, or want to put one together, please tell us about it.)

And then we get a clarification:

NOTE: Propagation to the initial containing block, the viewport, or the canvas background, of properties set on the html element itself is unaffected.

The logic, as I understood it (and taught it at these recent conferences) goeth thusly:

I think that logic makes good sense, and my expectations match the text of the spec as I read it. But all the browsers implemented something else.

Update :

According to browser engineers in the CSSWG, my explanation here wasn’t quite right.

The actual issue is that overflow propagates as defined in the spec, but containment remains on the root element. Since the overflowing content is contained, it is not visible to the viewport (where overflow is now set). Meanwhile, the root element (which can see the overflowing content) no longer has a specified value of overflow.

That is all proper according to the current specification. Any solution has to ensure that overflow and containment are applied to the same element – either the root or the viewport. Root is simpler, but doesn’t provide a number of scroll optimizations. On the other hand, it’s not clear what it would even mean for containment to propagate as well.

In the meantime, the solution below still works.

I am not a browser engineer, but I’ve been trying to parse out how browsers got a different answer than I did.

I’m not sure if browsers are ‘wrong’ when the root element has default visible overflow. The spec says to propagate from the body, but it doesn’t say what to do if containment breaks that propagation. I would expect the viewport to still default to visible overflow, which is then treated as auto. But that’s not stated explicitly, and the spec may need some improvements.

However, it does seem clear that a non-default overflow on root should propagate to the viewport with or without containment. It works fine without containment:

See the Pen Root overflow propagates fine by @miriamsuzanne on CodePen.

But when we add containment, our non-default overflow no longer propagates:

See the Pen Root overflow propagation plus containment by @miriamsuzanne on CodePen.

I filed an issue with the CSSWG.

Yes, with caveats. There’s a solution that works right now. Instead of setting height and overflow on the html element, we use the body as our top-level scroll-container. Here’s the code:

html {
  /* a size or inline-size container */
  container-type: size;
}

html, body {
  /* body and html both size to the viewport */
  block-size: 100%;
}

body {
  /* body is the root scroller */
  /* this value doesn't propagate */
  overflow: auto;
}

I made a codepen example that allows you to play with various combinations here, and see how each one behaves. With the combination above, the body is able to scroll, with fixed elements remaining in place:

See the Pen Testing a Root Container by @miriamsuzanne on CodePen.

Update :

This is an acceptable solution in many cases, but it comes with a trade-off. Browsers provide a range of optimizations for the ‘root scroller’ which won’t be applied to the body element here. Your mileage may vary.

But I would make one more change to the code above. By adding names to containers, we can better control what is being queried. I like to give containers both ID-style (unique) names, along with class-like (shared) names:

html {
  container: root layout / size;
}

Then we can explicitly query the root any time we want:

@container root (inline-size > 30em) { /* … */ }

Or we can query the nearest layout container:

@container layout (inline-size > 30em) { /* … */ }

Happy querying!

Recent Articles

  1. Article post type

    Generating Frontend API Clients from OpenAPI

    API changes can be a headache in the frontend, but some initial setup can help you develop and adapt to API changes as they come. In this article, we look at one method of using OpenAPI to generate a typesafe and up-to-date frontend API client.

    see all Article posts
  2. Stacks of a variety of cardboard and food boxes, some leaning precariously.
    Article post type

    Setting up Sass pkg: URLs

    A quick guide to using the new Node.js package importer

    Enabling pkg: URLs is quick and straightforward, and simplifies using packages from the node_modules folder.

    see all Article posts
  3. Article post type

    Testing FastAPI Applications

    We explore the useful testing capabilities provided by FastAPI and pytest, and how to leverage them to produce a complete and reliable test suite for your application.

    see all Article posts