Pontus Horn

CSS Containment without queries

This week I faced a problem I’ve faced many times before: I had an element that I wanted to occupy the full height of an ancestor higher up in the DOM structure. This ancestor was not a direct parent - there were several layers of intrinsically sized elements in between. To simplify it a bit, let’s imagine it looked something like this:

.header .button
A small box labelled .button with a grandparent labelled .header. The intermediate element's height is shrinkwrapped to the button's, and only takes up part of the header's height.

Here, I wanted the button to take up the full content box height of the header. In this illustration there’s only one intermediate container separating the two, but feel free to imagine some deeply nested monstrosity if you like. The point is, the button is not a direct child of the element whose height we want.

This is a classic CSS stumper. If you’re like me, your first instinct might be to set height: 100%. Even after many years of trying it in vain and learning why it doesn’t work, it’s still where my mind goes.

There are a number of ways to solve this, but I won’t go into them all here. This time when I saw the problem, I had another thought: is this a good use case for container queries? In my case, it was (with some caveats)!

My solution

Let’s take a look at the basic idea in code. We’ll set the header as a named size container:

.header {
  container: header / size;
}

This does rely on the header having an extrinsic height, meaning it’s either explicitly set or based on its own context. In this case, it had an explicit height, so no problem. If our styling relied on the header being intrinsically sized, this method would probably be a no-go.

Next up we’ll simply define our button’s height relative to the header’s:

.button {
  block-size: 100cqb;
}

And that works! Note that I didn’t have to define a container query for this to take effect. That was something I wasn’t sure about before.

.header .button
The same structure as before, but now the intermediate element and the button both stretch to fill the available content height of the header.

Note however that the value for block-size does not refer to the name of the container. This makes it a bit more brittle than I would like in the best of worlds. If the intermediate container defined its own block-size containment, that would take priority over the header’s. Wrapping it in a container query that references the container name doesn’t help either - the cq* units don’t care and just grab the closest applicable container.

For now, I don’t think there’s a way around that. Luckily the CSS Working Group is on the case and have resolved to add a function for every container query unit that allow to reference a named container - read the CSSWG Github issue for more details. If that lands in browsers, we might be able to do something like:

.button {
  /* Note: This syntax doesn't work at the time of writing */
  block-size: calc(100 * cqb(header));
}

Rad!

Another caveat is that when we set the height of the button this way, we’re ignoring any padding/border that the intermediate container might have. In my case above, it was more of a flexbox styling wrapper that’s not meant to have its own box styling, so it seemed like an OK tradeoff. Otherwise, this solution would probably not have been the way to go.

Conclusion

I’m happy with how container query units worked out for this use case. It helped me avoid having to style the intermediate container (which was in a different component) for the sake of one of its children. Sometimes that can’t be avoided, but when possible it’s nice to keep your styles close to where their intended effect are.

Another possibility in this specific case, since the header does have a fixed height, is to use the same variable that defined the header’s own height for the button itself. But then that wouldn’t automatically take into account any potential padding on the header, for example. The container approach is a very flexible and portable tool for this. If in the future we can reference which container we’re targeting here, all the more so.