A List Apart

Menu

Illustration by

Container Queries: Once More Unto the Breach

We’re a perseverant crew, the RICG. Responsive images were dead and buried forever in particularly dramatic fashion—what—two, three times there? “Dead and buried forever” refused to stick, though, thanks to all of you. Every time responsive images were knocked down, they got up a little stronger than before.

Article Continues Below

It isn’t hard to see why the going was so tough. Changing up a feature as old—and in software terms, as stable—as the img element was no small request. But designers and developers had spoken: we wanted a feature that stood to save our users a tremendous amount of bandwidth, and we carved out a seat at the same table as browser representatives and long-established standards bodies to make that feature happen. It was a long and failure-riddled process, but it’s what got us to the solutions we have today: not just one new element, but a whole suite of enhancements to the img element as well. Now we have options for smarter asset delivery based on any combination of viewport size, pixel density, and file format support.

After all that hard-won progress, there was no sense in packing up our GitHub organization and going home. We’ve changed the “I” in “RICG” from “Images” to “Issues,” and set our sights high right out of the gate: we aim to change the way we all write CSS.

The trouble with media queries

Styling a module meant to occupy multiple containers with viewport-based media queries means scoping that module’s styles to all the containers that it might occupy. That sentence isn’t the easiest thing in the world to parse, but it’s something I’m betting we’re all familiar with in practice.

The ideal responsive website is a system of flexible, modular components that can be repurposed to serve in multiple contexts. Using pattern libraries and tools like Pattern Lab by Brad Frost and Dave Olsen, we should be able to style the most fundamental parts of a website in a vacuum, assemble them into cohesive modules independent of the page, and then include them in the page layout in whatever contexts they were meant to occupy. But the reality isn’t as clear-cut as it might sound.

For the sake of discussion, let’s say we’ve been tasked with building a landing page for a store that sells Whitworth-based tools, a measurement system known only to those brave enough—or foolhardy enough—to own a very old vehicle of British make. I count myself among the latter group.

Wireframes showing four widths of a basic product page layout.

It’s a pretty simple design. The whole page is made up of product modules that occupy a large central container, and one identical “featured item” module that occupies a secondary container. The overall page layout only changes at a single breakpoint, where the “featured item” container shifts to a sidebar. Here’s a very sparse demo illustrating that layout change. The CSS for this is an easy read:

.col-a,
.col-b {
	clear: both;
	float: left;
}
@media( min-width: 960px ) {
  .col-a,
	  .col-b {
		clear: none;
	}
	.col-a {
		width: 70%;
	}
	.col-b {
		width: 27%;
		float: right;
	}
}

We really only have to deal with the modules in two different contexts, and only at our highest breakpoint: the primary container and featured containers are the same width until the featured container turns into a sidebar. We’ll put a class of .featured on that container so we can scope our styles to it later on.

Now that we have the page layout squared away, we can focus on the layout of the individual modules. They’ll switch between a vertical and a horizontal layout, whichever is best for the available space:

The vertical layout stacks the product name above the product image, and the description, price, and “add to cart” button fall below that. In the horizontal layout, the image is aligned to the left, while the product name, description, price, and other meta information are to the right of the image.

The vertical layout really doesn’t need much finessing in CSS. The natural flow of our markup does most of the work for us; we’ll just make a few small tweaks to constrain the size of our product images and center them beyond that size:

.mod img {
	display: block;
	margin: 0 auto;
	width: 100%;
	max-width: 250px;
}

The styles for the horizontal layout aren’t too much harder. For now, we’ll focus on the primary container—so we won’t scope these styles to our .featured class. Since we want the modules to go back to the vertical layout above 800px for the three-across layout, we’ll only apply the horizontal layout styles between 400px and 799px:

@media( min-width: 400px ) and ( max-width: 799px ) {
	.mod img {
		float: left;
		width: 30%;
		max-width: 999px;
	}
	.mod .mod-header,
	  .mod .mod-desc {
		float: right;
		width: 68%;
		padding-left: 2%;
	}
}

Here’s how those styles play out in a real page. They work just fine, up to a point—in our “featured item” container, where there will only ever be one module at a time, things fall apart a little at medium breakpoints:

Screenshot showing that, at medium breakpoints, the “featured” module is using the same vertical layout as the other modules on the page, but it’s much too wide. It has more than enough room to use the horizontal layout.

Not ideal, but fixable. We’ll just write a new media query to overlap with the one we wrote for the modules in the primary container, then scope all our styles to the .featured class we put on that secondary container element:

@media( min-width: 40em ) and ( max-width: 60em ) {
	.featured .mod img {
		float: left;
		width: 30%;
	}
	.featured .mod .mod-header,
	  .featured .mod .mod-desc {
		float: right;
		width: 68%;
	}
}

Well, okay. This works just fine—as you can see here—but it’s a little gross, code-wise. Not a DRY approach to writing CSS, by a long shot.

This is still bearable when we’re working with such a simple design, but it can get a lot worse when we start fine-tuning details. For example, the vertical layout calls for the “add to cart” button to appear on the right side of the module, but there isn’t enough room when we switch to the three-across layout at medium breakpoints:

Screenshot showing that the vertical layout pushes the “add to cart” and “quantity remaining” modules below the price.

If we finesse our styles so the “add to cart” and “quantity remaining” modules deliberately break below the product prices at these sizes—aligned to the left, instead of floating in space on the right—our stylesheet tangles a little more.

Switching between left and right alignment for these elements is simple in terms of the styles themselves: we’re really only dealing with text-align and float properties on the “quantity remaining” and the “add to cart” button. For the media queries that control those simple styles, though, we have a whole list of considerations to work around. We’ll need to add two breakpoints to both of the existing breakpoints that handle vertical alignment. Those styles will impact our “featured” module as well, so we’ll need to override those new styles using our scoping class, with media queries to match. Since the featured module has a little more space to work with than the modules lined up three-across, the “add to cart” button can move back to the right sooner in that context, at the highest breakpoint—yet another breakpoint. But wait, there’s more: these styles aren’t future-proof. If we should ever want to add a module to a new context, we’ll have to copy and paste all our styles over again, with a new scoping class and a fistful of new media queries. If the size of the page layout changes in any way—more or less padding, margins, any size-based styling change to the layout elements—we’ll have to adjust all of those many, many media queries. We now have to find a new viewport size where our breakpoints make sense. Our decisions are disconnected from our layout.

This page comes nowhere near the level of complexity we might encounter in a real project, but we already have a tough stylesheet to maintain.

Enter element queries

All this complication comes from our reliance on the viewport to make our styling decisions. Media queries work just fine for our high-level page layout, but they’re nowhere near as useful for modular components. What we really need is the ability to style page components based on the space they occupy rather than the size of the viewport.

“Element queries” assume a syntax that does exactly that. We’re accustomed to writing a media query for styles that apply only when the viewport is greater than 40em, like this:

@media( min-width: 40em ) {
}

Instead, imagine a syntax where the query itself is scoped to an element and populated with styles that only apply when that element is larger than 40em:

.mod:media( min-width: 40em ) {
}

Well, this would change almost everything we’ve written for our Whitworth page so far. We might still use media queries for our page’s one major layout change, where we go from a linear layout to a larger primary element and a sidebar; basing that on the viewport size makes sense. Once we started building our modules themselves, though, this new syntax would allow us to write all the styles they could ever need just once:

.mod img {
	display: block;
	margin: 0 auto;
	width: 100%;
	max-width: 250px;
}

.mod:media( min-width: 425px ) img {
	float: left;
	width: 30%;
	max-width: 9999px;
}
.mod:media( min-width: 425px ) .mod-header,
.mod:media( min-width: 425px ) .mod-desc {
	float: right;
	width: 68%;
	padding-left: 2%;
}

That’s it, those are all the styles our module layouts will need—see for yourself in this demo, which uses a JavaScript shim to make this theoretical syntax work for real. When the module is larger than 425px, the browser uses the horizontal layout. If the module is smaller than 425px, the browser uses the vertical layout. The styling is now 100% dependent on the container that the module occupies—if it has enough space for the horizontal layout, those are the styles that get used. If we changed the three-across layout to two-across with each module occupying half the container, or we introduced a completely new and unpredictable context for one of these modules, we wouldn’t need to change a thing about their styling. Imagine how much more useful a pattern library would be if we removed the contextual dependency from these kinds of standalone modules: you’d be able to grab any component from the page and move it to any container in the page, without needing to rework hundreds of lines of CSS—without reworking any CSS at all.

For fine-tuning details like the position of the “add to cart” button and quantity remaining, things wouldn’t really get any more complex. Since we wouldn’t be dealing with scoping or finessing module breakpoints to match the viewport-based layout breakpoints, we’d only have one concern: “Is the module too small to have the price and ‘add to cart’ button side by side?” A little experimentation tells us that there’s enough room for both elements as long as the module is larger than 320px, and that’s where we’ll put our module-based breakpoint:

.mod:media( min-width: 320px ) .mod-cost {
	float: left;
}
.mod:media( min-width: 320px ) .mod-buy {
	float: right;
}
.mod:media( min-width: 320px ) .buy-remaining {
	text-align: right;
}

From dozens of lines of redundant CSS, media queries, and overrides to nine lines containing the styles we need. More important, nothing would break if any other styles on the page should change. You can try this out for yourself by fiddling with styles in your browser’s dev tools in this demo, which is using a script that mimics element query behavior—it isn’t something you’d want to use on a production website, but it’s a great way to get your head around the concept.

If this syntax existed, we’d be finished with this Whitworth project in just a few lines of CSS.

But element queries can’t exist—at least, not the way we’d been thinking about them. Just like the first version of the picture specification, element queries are dead and buried forever. But like the search for responsive images, we’re not about to let that stick—not when we have such a clear problem to solve.

Exit element queries

This concept isn’t a hard sell—not for us developers, anyway—but neither were responsive images. Once we get into the gritty implementation details, though, things change.

The first step for the RICG was framing out a Use Cases and Requirements document for element queries, describing and diagramming the problem—not proposing a solution, just providing a clear problem to be solved. So far it’s shaping up to be a lot more focused than the Use Cases and Requirements document for responsive images because it really only sets out to solve one overarching issue.

As word of this document got around, people started thinking through the problem a little further, just like we’d hoped. That led many of us to the same conclusion, before we’d even started tinkering with a proposed specification: element queries can never work.

This has been documented in more detail elsewhere, but the gist is this: we’re using CSS to control CSS, and that means we can cause infinitely looping style changes. If we can tell an element to restyle itself using an element query, what happens if we use that element query to change the element’s width to one where the element query no longer applies?

.our-element:media(min-width: 500px) {
	width: 499px;
}

Well, since the query no longer matches, the new width is no longer applied. Since that new width is never applied, the element query would match again, so the new width would be applied, so the query would no longer match, so the new width wouldn’t be applied—and so on unto infinity. We’ve achieved a TARDIS-caliber paradox with just a few lines of CSS, and there’s no predictable way for the browser to handle it. Should the browser, upon encountering one of these situations, throw the element query styles away entirely? Only throw away the new width? Uninstall itself from the user’s machine? Start a small-to-medium-sized fire? Nothing sounds particularly appealing about any of this, least of all from a browser maker’s standpoint. They’ll never let something with this kind of error potential see the light of day. Game over.

Exciting news, right? We figured out that element queries were impossible in a fraction of the time it took to figure out that responsive images were impossible. The first time, I mean.

Okay, hear me out on this. “Element queries” are over, sure, but the problem hasn’t changed—the Use Cases and Requirements document isn’t going anywhere. This still needs to be solved, and now we know how not to solve it: elements can’t be restyled based on their own properties.

Enter container queries

To solve the problem outlined in the Use Cases and Requirements document, we need to reframe the way we talk about a potential solution. Since a solution can’t allow an element to restyle itself, we can build that constraint into the specification: queries attached to an element can only influence the styling of that element’s child elements.

Armed with our new knowledge of the impossible, the search for element queries has been reframed as container queries.

What would this change about our Whitworth project? Well, nothing—we were only styling the child elements of .mod anyway, not .mod itself. It might just mean a few behavioral changes in terms of how the browser treats these elements. The browser couldn’t allow an element with an attached query to be influenced by the size of its child elements, for example. We might end up with an implicit browser-level overflow style on any element with a query attached, to prevent the infinite looping behavior—much more predictable than the browser picking and choosing bits of our stylesheets to avoid melting down.

So, how do we keep things moving forward from here? By finding the next impossible-to-solve issue. So far, “container queries” have held up to more scrutiny than “element queries” did—but talk is cheap. This recursion issue is a big one, but it still only scratches the surface of the potential issues we could run into as a solution starts to materialize.

We’ll make a lot more progress by experimenting with this pattern, which isn’t the easiest thing to do when we’re dealing with syntaxes that don’t exist yet. That’s how we ended up with the very first version of Scott Jehl’s Picturefill, lo those many years ago. Without tinkering—without actually using something resembling the picture markup pattern—we couldn’t make much sense of the spec we were just starting to write. So in the same vein, the RICG has started up a repository for container query demos, using the same shim as our Whitworth demos. Now we can all try this syntax out for ourselves, and help find the issues we’ll run into when using this kind of CSS pattern for real—the sorts of problems you can only find by building something. Clone the repo, copy the demo-template directory into /demos, give your demo a name, and build something using container queries. Be sure to let us know how it goes, whether good or bad.

Exit container queries..?

As the first version of the container query spec comes together, well, its days may very well be numbered, too. The Use Cases and Requirements document isn’t going anywhere, though; the problem we need to solve is out there, looming, demanding that it be solved somehow.

The answer may be this very first proposal, for all we know. It might be something completely different—something we haven’t even begun to consider. But the RICG isn’t a decision-maker in the world of web standards, and we don’t aim to be one. We want to be a rallying point—to provide ways for designers and developers who might not want to wade through arcane listservs and IRC channels to get involved in the standards that affect their daily work.

There’ll be plenty more work to do after this, and we’ll need all of you to get it done. With your help—with thousands of designer and developer voices contributing to the search—it’s only a matter of time until we find the answer together. We’ve done it before, and we’ll do it again.

Stiffen the sinews; summon up the blood. Banish the phrase “element queries” from your vocabulary forever. Let’s get to work on container queries, regardless of what name or form they might take in the future.

26 Reader Comments

Load Comments