Dec 24

Container Style Queries

Exploring new ways of approaching CSS by querying custom properties and their values.

By Manuel Matuzovic

One of the most requested features in CSS was container queries, the ability to query the size of a parent container. Before container queries, you could only query the size of the viewport. That worked well and still does, allowing you to create responsive websites, but it's really just a compromise because, in most cases, we're interested in the size of our parent container, not the size of the viewport.

That’s why container queries are such a critical addition to CSS.

In this example, when the .cards container reaches a minimum width of 60rem, set the flex-grow value of each .card within the container to 1.

.cards {
container-type: inline-size;
display: flex;
}

@container (inline-size > 60rem) {
.card {
flex-grow: 1;
}
}

I find container style queries even more interesting than container size queries because they could significantly change the way we think about and write CSS. Join me as we explore the reasons why.

The Theory

The difference between size and style queries is that with style queries, you don't query a predefined list of features but use the style() function to query the value of a CSS property.

If you wanted to apply styles to an element only when one of its parent elements has a display property with the value flex, you could do that by querying style(display: flex).

Here, when the .cards container's display property is set to flex, add a border to all but the .card that is the :last-child.

.cards {
display: flex;
container-name: cards;
}

@container cards style(display: flex) {
.card:not(:last-child) {
border-inline-end: 2px solid;
}
}

Unfortunately, I can't provide you an interactive demo because style queries with normal properties aren't supported in any browser and might never be because there are still concerns about its feasibility.

The practice

What you can do today, at least in supporting browsers, is query custom properties.

Let's take the following example: We have a red button, a regular section with a transparent background, and a highlighted section with the same red background as the button.

See the CodePen.

The button works well in the regular section but almost vanishes in the highlighted section.

You could select all buttons in that section (.section--highlight button) or create a class with alternative styling for buttons to work around the issue.

See the CodePen.

That works, but it's a local solution to a global problem. It fixes the bug, but it doesn't solve your general issues:

If you've built larger, themeable systems, these problems are not news to you, and you know how to solve them.

One solution is to define custom properties on the highlighted section that descendants can inherit.

See the CodePen.

That frees you from applying classes to your buttons and automates the process but also ties your button styles to the highlighted section, which doesn't scale well, and you can’t reuse the button styles.

Container style queries offer a more global and systematic solution: You can add a container query that checks whether any parent element has a red background color (--container-bgc: var(--red)). If that's the case, its descendant buttons get custom styles.

Please note, I've renamed the --section and —-body prefix to --container to make it globally applicable.

See the CodePen.

That's already pretty great because the button styles change depending on the value of the --container-bgc property and not on the kind of parent element.

The only problem with this solution is that you can't reuse those alternative button styles elsewhere. You could try to make them reusable, similar to a mixin in Sass, by creating another query to apply them whenever a container has the --button-style: secondary property and value.

/* Sets the buttons styles when a parent element has the custom property */
@container style(--button-style: secondary) {
button {
--button-bgc: var(--white);
--button-color: var(--red);
}
}

/* Uses the button styles defined earlier when the background is red. */
@container style(--container-bgc: var(--red)) {
button {
--button-style: secondary;
}
}

That doesn't work, though, because the properties in the first query apply if a parent element of the button has --button-style: secondary set. Still, the second query sets the property on the button itself, not a parent element.

The absence of containers is a problem you'll often face when working with container queries. Miriam Suzanne, co-author of the containment specification, suggests Turtle Components™️ - elements that come with a shell (an extra container).

<body> <!-- shell -->
<main class="container-inner"> <!-- new! 🐢 -->
<section> <!-- shell -->
<div class="container-inner"> <!-- new! 🐢 -->
<h2>Default section</h2>
<button type="button">Let it snow!</button>
</div>
</section>

<section class="section--highlight"> <!-- shell -->
<div class="container-inner"> <!-- new! 🐢 -->
<h2>Highlighted section</h2>

<button type="button">Let it snow!</button>
</div>
</section>
</main>
</body>

The content of each section is wrapped in an extra div.container-inner, as well as the whole main content.

When the parent container has the right background color, styles apply to the inner container element.

In this update, we query the parent section, apply properties/settings to the .container-inner div to which other queries can react.

See the CodePen.

I know what you’re thinking: "That’s over-engineering par excellence! Why would you want to add so much complexity?"

For one, it scales well! If you add a completely different element with the same background color, its text and the button adapt accordingly. You don’t need any extra steps.

Here's an entirely new component with the same concept of default and highlighted background color.

<div class="card">
<div class="container-inner">
<h3>I'm a card</h3>
<p>Lorem ipsum dolor sit.</p>
<button type="button">Let it snow!</button>
</div>
</div>

<div class="card card--highlight">
<div class="container-inner">
<h3>I'm a card</h3>
<p>Lorem ipsum dolor sit.</p>
<button type="button">Let it snow!</button>
</div>
</div>

Just set the background color and the button will adapt accordingly.

.card {
background-color: var(--container-bgc);
}

.card--highlight {
--container-bgc: var(--red);
}

Admittedly, you could achieve a similar result by creating a helper class that does the same thing by setting all the necessary properties.

<div class="card h-theme--highlight">
<div class="container-inner">

</div>
</div>
.h-theme--highlight {
--container-bgc: var(--red);
--container-color: var(--white);
--button-bgc: var(--white);
--button-color: var(--red);
}

You can do that, and you might find it more intuitive, but like I said in the introduction, container style queries can change how you approach CSS. They enable you to reduce the dependency of the class as a middleman between your markup and your styles. Not that classes are bad in any way, but it's worth exploring how you can automate the process of adding conditional styling to your components using classes in HTML by writing conditions in CSS.

You can even take it further and eliminate the modifier classes, and it would still work because your container queries look for custom properties on the element.

<section style="--container-bgc: var(--red);">
<div class="container-inner">
<h2>Highlighted section</h2>

<button type="button">Let it snow!</button>
</div>
</section>

See the CodePen.

If you want to support more colors, all you need to do is extend the query. This will apply custom styles if the background color is red or gray.

See the CodePen.

If you want to add dark mode to your site, all you do is change the --container-bgc property on the body.

@media (prefers-color-scheme: dark) {
body {
--container-bgc: var(--gray);
}
}

See the CodePen.

The level of abstraction you pick depends on the size of your design system and who's using it. Suppose you're not the only one working with the components you build, but different teams and third parties. In that case, investing some extra work to create a resilient and flexible architecture that automates certain rules while improving the developer experience makes sense.

Container Style Queries may increase complexity for you, but they can decrease it for people using your components.

Use cases

There are also other use cases that work for projects of any size. Here are some examples.

Page-level settings

Especially in large systems, but also on smaller sites you have different variations of page themes, layouts, and styles. With container queries, you can give authors a simple and human-readable way to tweak page styles.

For example, using custom properties on the root element to make decisions about the page’s theme and styles.

:root {
--theme: light;
--style: rich;
}

You can edit the CSS in this Pen directly in the light grey area.

See the CodePen.

Component-level settings

You can provide a theming API for components.

.card {
--card-size: m; /* s, m, l */
--card-theme: light; /* light, dark, colorful */
}

See the CodePen.

Inheritance

The great thing about custom properties is that they're inheritable. If you want to change the size of all cards in a container, you apply the settings to the container instead of each card separately.

<div class="cards" style="--card-size: s;">
<div class="card"></div>
<div class="card"></div>
<div class="card"></div>
</div>

Or in CSS, where the --card-size setting will apply to all nested cards.

.cards {
--card-size: s;
}

See the CodePen.

Relational styles

Using the :has() pseudo-class, you can apply styles to an element if it has a specific child element.

.card:has(img) h1 {
background: var(--gray);
color: var(--white);
}

With container style queries, you can do a similar thing in CSS. In the following example, styles only apply if the element has a background image. You're checking the existence of a particular property.

In this style query, the .content gets a background color and a different text color when its parent element has a background image.

.card-inner {
--card-background-image: url('background.jpg');

background: oklch(100% 0 0) var(--card-background-image);
background-size: cover;
}

@container style(--card-background-image) {
.content {
background-color: oklch(30% 0 0);
color: oklch(100% 0 0);
padding: 0.2em 0.6em;
}
}

See the CodePen.

Mixins

There is a proposal for mixins, but it still needs to be completed. With container style queries, you already have some of the functionality mixins would offer: reusability.

Let's take this simple media query. A small button becomes larger at a minimum width of 30em.

button {
font-size: var(--font-size, 1.2rem);
border: var(--border-width, 2px) solid var(--border-color, transparent);

/* other button styles */
}

@media (min-width: 30em) {
button {
--font-size: 1.6rem;
--border-width: 4px;
--border-color: oklch(from var(--red) calc(l - 10) c h);
}
}

If you wanted to reuse those styles somewhere else, you'd have to repeat them.

.btn--large {
--font-size: 1.6rem;
--border-width: 4px;
--border-color: oklch(from var(--red) calc(l - 10) c h);
}

Instead, you can create a custom property that takes those styles and puts them wherever you want.

<div class="btn">
<button type="button">
Let it snow!
</button>
</div>

<div style="--btn-style: large">
<button type="button">
Let it snow!
</button>
</div>

<div class="btn--large">
<button type="button">
Let it snow!
</button>
</div>
button {
font-size: var(--font-size, 1.2rem);
border: var(--border-width, 2px) solid var(--border-color, transparent);
}

@media (min-width: 30em) {
.btn {
--btn-style: large;
}
}

.btn--large {
--btn-style: large;
}

@container style(--btn-style: large) {
button {
--font-size: 1.6rem;
--border-width: 4px;
--border-color: oklch(from var(--red) calc(l - 10) c h);
}
}

See the CodePen.

It's not really a mixin because you can't pass parameters, but you can reuse styles without duplicating them.

Summary of technicalities

Before we wrap this post, let me quickly summarize some technicalities that are worth knowing.

Type and Name

Every element is a style container; you don't have to set the type explicitly.

A query always queries the closest parent container unless you provide a container name in the query.

<main> <!-- container "page" -->
<div class="wrapper"> <!-- container "wrapper" -->
<h1>Ho! Ho! ho!</h1>
</div>
</main>
:root {
--gray: oklch(30% 0 0);
--white: oklch(100% 0 0);
--green: oklch(0.56 0.16 134.58);
--red: oklch(0.56 0.26 28.58);
}

main {
container-name: page;
--style: var(--red);
}

.wrapper {
container-name: wrapper;
--style: var(--green);
}

/* Evalutes to `true` because it is looking at
the --style property on `main` */

@container page style(--style: var(--red)) {
h1 {
border: 2px dotted;
}
}

See the CodePen.

Name and value

You can query custom properties without providing a value.

@container style(--card-background-image) {
.content {
background-color: oklch(30% 0 0);
}
}

/* This would be possible if style queries would work with normal properties, too */
@container style(background-image) {
h1 {
background-color: oklch(0% 0 0);
}
}

Comparison

When you're comparing two unregistered custom properties, you are comparing tokens.

The following query is true because you’re checking whether “red” is “red”.

html {
--color: red;
}

.child {
--bg: red;
}

/* Condition is true because `red === red`, styles applied */
@container style(--bg: var(--color)) {
h1 {
border: 10px dotted fuchsia;
}
}

The following query, however, is false because the string token "red" doesn't equal the hash token "#F00", although they represent the same color.

html {
--color: #F00;
}

.child {
--bg: red;
}

/* Condition is false, styles not applied */
@container style(--bg: var(--color)) {
h1 {
border: 10px dotted fuchsia;
}
}

To compare colors, you must register the property you’re querying as a <color>.

@property --bg {
syntax: "<color>";
inherits: true;
initial-value: transparent;
}

html {
--color: red;
}

.child {
--bg: red;
}

/* Condition is true, styles applied */
@container style(--bg: var(--color)) {
h1 {
border: 10px dotted fuchsia;
}
}

See the CodePen.

Due to property substitution, you don't have to register the second property, as well.

Conclusion

One of the most significant downsides to some of the concepts I showed in this post is that you can't query an element itself; you must query a container. That’s not so much a problem because of the extra divs you may have to add (you probably already have enough divs in your markup that you could use as containers anyway). It's that in some situations, applying styles to a container isn't intuitive. Sometimes, you'd want to apply them directly to an element.

button {
--btn-style: funky;
}

(You can actually do that already by making use of Roma Komarov’s mind-boggling Cyclic Dependency Space Toggles.)

Nevertheless, container style queries are exciting and can make your lives or the lives of those using your components easier. At the moment, Chromium-based browsers are the only browsers that implement them. Safari is willing to implement, but we have yet to learn about Mozilla's position.

So, please play around with container style queries, and if you like them, make some noise to convince other browser vendors of their usefulness.

Manuel Matuzovic

Manuel Matuzovic

Manuel is a freelance frontend developer, accessibility auditor, teacher, and consultant who’s passionate about the web. He writes about accessibility, HTML, and CSS on his personal blog matuzo.at and on htmhell.dev.

Manuel selected SOSBalkanroute for an honorary donation of $50

SOSBalkanroute

SOSBalkanroute is a humanitarian initiative for a dignified life for refugees in south-east Europe.