Thinking about a new responsive web

A deep dive into container queries

Dec 15th, 2022

css

Design on the web has changed drastically over the years; So much so that we now have the tools to build on ideas that we once thought were impossible. It's an exciting time to be a front-end developer, and I don't say this lightly! There was a time when structuring layouts on the web was very tedious, and implementing responsive layouts was not even a question in mind. Developers were forced to build multiples website to ensure that desktop and mobile users had a great user experience when visiting a website.

It's important to remember that user experience drives the design of user interfaces, and layout is a critical component of those interfaces. Therefore, as the complexity of those interfaces grows, so do the options for designing layouts for them.

Let's think about the following questions as they relate to responsive layouts:

There was a time when questions like the ones above were difficult to answer, but the introduction of the responsive web, allowed us to begin uncovering the answers to those questions.

A look at where we are

Tools such as media queries, flexbox, and CSS grid allowed developers to reshape layouts on the web to suit the devices of many users. Media queries brought us the ability to query the size of the viewport, thereby allowing us to modify the CSS properties on DOM elements with a change in viewport size. This addition meant that we could now show/hide and even re-order DOM elements based on the viewport size.

Let's imagine that this box is our viewport. As we make the size of this viewport smaller (dragging the slider), we begin to see a layout shift -- Elements that were once in rows are now put into columns, and elements that were once present are now hidden.

min width

max width

John Doe

Staff Developer

John Doe

Staff Developer

This can be observed on any modern website, including this one! Try changing the size of your web browser!

Flexbox brought about the idea of flexible containers and items, and it allowed us to explore what happens to the intrinsic size of an item as the available space changes. Our options grew as we could now begin to wonder how elements could distribute the available space to properly fit within the viewport. Thereby allowing us to ask deeper questions about what it means to have a more flexible/fluid layout. This is of course in addition to our ability to hide elements or move to a column layout at smaller screen sizes with media queries. Josh Comeau has an incredible article that explores Flexbox and its awesome quirks -- it's worth the read!

CSS grid, commonly known as Grid, brought about the idea of a two-dimensional grid system that redefined our approach to developing user interfaces. It allowed us to think of our page as a grid system, thereby making the elements on our page items on that grid. This allowed us to forgo workarounds for implementing certain layouts, as now we could place items anywhere within our predefined grid system. As with Flexbox, CSS grid is another tool in our arsenal that we can use in conjunction with media queries and Flexbox to create responsive layouts.

Although we are still iterating and uncovering answers to questions that drive the user experience on websites, the shift to component-based development has made it increasingly important to think about how components define their own layout. This can be defined as macro & micro layout; macro layout being the layout that defines the overall page structure/layout and micro layout being the layout that defines the intrinsic layout of a component. The tools described above have allowed us to iterate on macro layouts but as development has shifted towards more component-based layouts, we have to iterate on development with micro layouts.

Container queries

Container queries iterate on the idea of micro layouts, as they allow us to set a defined container and have the children query the size of the container. Similar to media queries, container queries allow us to modify the CSS properties on DOM elements, thereby allowing us to show/hide, re-order DOM elements, and modify other properties based on the size of a container. An important distinction to be aware of is that media queries allow us to query the size of the viewport, whereas container queries allow us to query the size of a defined container.

Container queries move us beyond considering only the viewport, and allow any component or element to respond to a defined container’s width -- Stephanie Eckles.

This solves a unique problem in that container queries allow us to develop components that are intrinsically responsive. This allows us to have confidence that we can build a component once, but use it anywhere. More specifically, a component that is used in the main section of a web page can now be put in a sidebar without adding any additional utility classes that target the component in the sidebar.

Let's check out the example below in which we've defined some components that make use of container queries.

Title Example

A bit of desc text in here

Title Example

A bit of desc text in here

We can see that the same component is being rendered differently depending on the available space. When there is not enough space, the black box and text stack. But when the space becomes wide enough, the black box and text can now be side-by-side and the title can have a bold font weight.

Getting started with container queries

Container queries were recently introduced into the CSS spec, and as of the time of writing, they are available for use across the major browsers. The first step to getting container queries to work is by setting containment on a parent element. Containment allows developers to tell the browser what parts of the page are encapsulated as a set, thereby providing isolation of a DOM subtree from the rest of the page. Moreover, it's hinting to the browser which parts of the page can be treated as independent, therefore, setting containment on an element enables browsers to isolate queries for that container. Four types of containment can be set on an element, and they include:

  1. size
  2. layout
  3. style
  4. paint

If you've been following the formation of the container query spec then you may recognize that the contain property was used to define a container. Using the contain property, you would have to set style layout and size containment at the same time to properly define the container. However, there have been revisions in the container query spec and it's now a lot simpler to set containment on an element. container-type allows us to specify the size containment on an element, while the style and layout containment are automatically added.

Let's say that we had the following HTML snippet, and we wanted a section that appears both in our main section and inside a sidebar:

html

<main>
<section>...</section>
<section class="container">...</section>
<section>...</section>
</main>
<aside>
<section class="container">...</section>
</aside>

In our CSS we would select our section that has the class of container and set our container-type property on this selector. Since container-type is a shorthand for setting the size containment, it has one of three values:

  1. inline-size - Establishes queries on the inline axis of a container.
  2. size - Establishes queries on both the block and inline axis of a container.
  3. normal - Does not establish a query container for any container size queries but remains a query container for style queries.

Since we only want to query for the width of the container, we're going to set the container-type to inline-size.

Things to be careful about

By setting container-type to inline-size we are telling the browser that the container itself and not its children is responsible for setting its size in the inline direction. This means that we have to explicitly specify the size of the container in the inline direction. For the English language, this would be the width of the container.

However, when we change the container-type to size, we are telling the browser that the container itself and not its children is responsible for setting its size in both the inline and block direction. This means that we have to explicitly specify the size of the container in both the inline and block directions. For the English language, this would be the width and height of the container.

Most often we will be using the inline-size when setting the container-type on an element.

Although optional, we can also name our container with the container-name property. This may come in handy if we were dealing with multiple containers or even nested containers. A nice shorthand for both the container-type and container-name is the container property. Using this property, we can specify the container-type and container-name in a single line. It's as follows:

css

.container {
container: container-name / container-type;
}

Now that we've defined our container, we can begin writing our queries to set styles for the children. A queried element will use its nearest ancestor that has containment applied. This is important to keep in mind because nesting containers is possible and, as you may recall, we were speaking earlier about how the container-name property may come in handy when nesting containers. Further, if we are trying to query a container when there are no containers defined, then the query itself would be disregarded. It will fall back to the version of the styles applied to elements before the query.

So, how do we write a container query? As I alluded to above, container queries are similar to media queries, and these similarities extend to their syntax. Container queries begin with @container and are then followed by the optional container name and then the query parameter. It looks something like this

css

@containter optional-name-parameter (min-width:300px) {
... things go in here;
}

It's important to note that we are querying against the computed min-width rather than the defined style of min-width -- this may be useful when setting a container-type on an element that is a flex item or a grid item. Furthermore, the rules applied inside a container query only affect the descendants of the container and not the container itself -- ie/ containers cannot query themselves. Going back to the HTML markup above, we cannot apply rules to the section that has the class of container if that element is a container, and is being used to set a query. However, we can apply rules to the descendants of elements inside the container.

Let's take a look at what writing a container query would look like for a card component:

.App {
  font-family: sans-serif;
  text-align: center;
}

.Container {
  display: flex;
  justify-content: center;
  gap: 1em;
  flex-wrap: wrap;
}

.card {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 2em;
  padding: 1em;
  background-color: rgba(217, 217, 217, 0.5);

  border-radius: 10px;
  flex: 1 0 200px;

  container-type: inline-size;
}

.cardImg {
  font-size: 2rem;
}

@container (min-width:300px) {
  .card_User {
    display: flex;
    gap: 3em;
  }
}

Looking at the CSS file, we can see that we've set containment on the element that has a class of card. We're now going to be querying the container to apply some styles when the container is larger than a certain size -- in this case when the width is greater than 300px. Here we are changing the flex orientation to a row and using a larger font size. This is so awesome because this is now an intrinsic layout as we're specifying styles based on the size of the container rather than the size of the page.

Structuring our queries

The container/media query similarities also extend to the way we approach structuring the queries. There are many different thoughts and approaches to properly structuring queries but I find adopting a "mobile-first" approach is the most intuitive. I say "mobile-first" as we can start with the smallest layout as the default and then progressively query on larger container sizes.

Earlier we mentioned that a container cannot query itself, however, a container can be used as part of the CSS selector for its children. Meaning that you can use a container as a compound selector or a way to select its descendants.

Container queries in a flex or grid layout

When working with flex or grid layouts, it may be tempting to think of flex/grid containers as the container in which you want to query to change styles within the flex/grid item. Although doing this may be useful in some situations, in most situations it often yields unexpected results. Most often, the flex/grid container is part of the overall page layout, which then responds to the size of the viewport. Therefore, putting a container-type property on a flex/grid container that is part of the overall page layout makes the container query act more like a media query. This is the case as the computed width of those flex/grid containers only changes as the size of the viewport changes.

We're now in this catch-22 as:

  1. We can't put the container-type property on elements that we want to style as a result of our query -- IE/ containers can't query themselves
  2. We may end up creating a container query that acts more like a media query

How do we solve this? 🤨

We're going to put the container-type property on a flex/grid item, however, the flex/grid item may not be the element that you think.

html

<main>
<section class="container"><p>...</p></section>
<section class="container"><p>...</p></section>
<section class="container"><p>...</p></section>
</main>

We're going to wrap our elements in a wrapper class which then becomes the flex/grid item to which the container-type property is added. To be specific, main would be a flex/grid container while section elements with the class of container would have the container-type property added in CSS.

We spoke a bit earlier about using the container-type property with elements that were either a flex item or a grid item. Moreover, we established that the container query parameter uses the computed width rather than the defined style width. This rule is especially important for flex-items as the width or flex-basis that is set for that item is more of an idealized value -- Josh Comeau speaks about this more in this article. Therefore, when querying for a width of 300px, the query parameter is going to be looking at the computed width of the flex-item rather than the width or flex-basis that is set on that item. This is also important for grid-items in which the column size is set using the fr unit.

Pop Quiz!

Question: What happens when you set inline size containment on a flex-item that does not have a width defined or whose flex-basis is set to auto?

I think it's important to be aware of the relationship between containment and flexible items, especially as we've moved from rigid layouts to more fluid and flexible layouts. It's weird to think about, as container queries have removed intrinsic sizing on elements that have a container defined. But if those elements are also flexible, then they retain the ability to grow and shrink, thus the container itself is now flexible. I think this establishes a new dimension that adds to the fluidity of micro-layouts, as components that define their own layout can now respond to their environment.

Consider the following - Drag the slider to change the width of the container and observe what happens to the layout of the cards. Just so you're aware, each of these cards is a flex-item.

min width

max width

profile pic

Michael Lawson

Design Lead

contact me

profile pic

Lindsay Ferguson

Project Lead

contact me

profile pic

Tobias Funke

Developer I

contact me

The little big things

Container queries have some side effects that would be useful to know. We spoke a bit earlier about how setting containment on an element allows developers to tell the browser what parts of the page are encapsulated as a set, thereby providing isolation of a DOM subtree from the rest of the page. Since setting containment also includes setting layout containment, we are telling the browser that everything needed to construct the layout of this element and its descendants is scoped within the element itself. Meaning that the browser does not have to look outside that element to know how to construct the layout for that element and its descendants.

Having the layout scoped to the element means that we establish a new formatting context thus no more collapsing margins. This is similar to layout algorithms like flex and grid in that these algorithms do not have any collapsing margins aswell. Further, the element that gets containment will also be the containing block for fixed and absolutely positioned elements -- this is similar to putting position:relative on a parent element. You also create a new stacking context, so you now have the ability to use the z-index property.

Container queries also introduce a range of new units that add to the fluidity and flexibility of a container. These units are similar to viewport units in that they are relative units but instead of being based on the size of the viewport, they are based on the size of the container. These units include:

Caution

For the container query units, if there is no container defined then units will be looking at the viewport for a definition. This means that 1cqw will equal to 1vw.

Further, if your container-type is set to inline-size then cqb will act like a viewport height since it is unaware of the height of its container. This changes if container-type is set to size. Moreover, cqmin and cqmax start working properly if the container-type is set to size.

I want to bring your attention back to the demo above -- the demo with the user cards and slider. If you look closely, you'll notice that the font size of the name changes as you change the size of the container. This is really cool because now we can create variable fonts sizes that are scoped to the size of the container. Variable font sizes have been a thing for a while because of the introduction of viewport units, however, we can now make these font sizes respond to the size of the container rather than the viewport. This is made possible by the container query units.

The impressive thing is that this variability is not limited to just the font size. We can also create variable padding and margin sizes. We may want small amounts of padding when the container has limited space, but as we increase the available space inside the container, we may also want to increase the padding as well.

Wrapping up

Design on the web has changed drastically over the years as we've moved from intuiting hacks that solve obscure problems about layout on the web to an era of responsive design. These problems and hacks have allowed us to think about what it means to have a webpage that responds to the type of device it's presented on -- bringing about layouts that move away from rigidity, and towards fluidity. We're now in a new era of design on the web!

Jen Simmons said it best when she said, "we're now in the era of intrinsic design". Container queries are something special as they allow us to think about what it means to have a responsive component. Moreover, they've allowed us to overcome the last hurdle of what it means to have a component design system, as components can now own and define their own layout. We can now truly create a component once, but use it everywhere!

Container queries however do not stop at querying for just the size of the container. Just as media queries now allow us to query for user preferences, in the future we may be able to query for more than just a containers size. There is experimentation being done on what it means to query the style or state of a container. If a container has a certain style or is in a certain state (is the container currently stuck or not?), what styles can we apply to its children? Geoff Graham explores container style queries in the following article and Ahmad Shadeed's new article diagrams different use cases for style queries.

It's an exciting time to be a frontend developer, as the future of design on the web looks bright and exciting!!

All right, I'm going to wrap it up there! Hope you found this useful, and I'll catch you in the next one... Peace!

on this page

a look at where we arecontainer queriesgetting started with container queriescontainer queries in a flex or grid layoutthe little big thingswrapping up

Last updated December 19th, 2022