A Primer On CSS Container Queries

A Primer On CSS Container Queries

At present, container queries can be used in Chrome Canary by visiting chrome://flags and searching for and enabling them. A restart will be required.

Note: Please keep in mind that the spec is in progress, and could change at any time. You can review the draft document which will update as the spec is formed.

What Problem Are CSS Container Queries Solving?

Nearly 11 years ago, Ethan Marcotte introduced us to the concept of responsive design. Central to that idea was the availability of CSS media queries which allowed setting various rules depending on the size of the viewport. The iPhone had been introduced three years prior, and we were all trying to figure out how to work within this new world of contending with both mobile screen sizes and desktop screen sizes (which were much smaller on average than today).

Before and even after responsive design was introduced, many companies dealt with the problem of changing layout based on screen size by delivering completely different sites, often under the subdomain of m. Responsive design and media queries opened up many more layout solutions, and many years of creating best practices around responding to viewport sizes. Additionally, frameworks like Bootstrap rose in popularity largely due to providing developers responsive grid systems.

In more recent years, design systems and component libraries have gained popularity. There is also a desire to build once, deploy anywhere. Meaning a component developed in isolation is intended to work in any number of contexts to make building complex interfaces more efficient and consistent.

At some point, those components come together to make a web page or application interface. Currently, with only media queries, there is often an extra layer involved to orchestrate mutations of components across viewport changes. As mentioned previously, a common solution is to use responsive breakpoint utility classes to impose a grid system, such as frameworks like Bootstrap provide. But those utility classes are a patch solution for the limitations of media queries, and often result in difficulties for nested grid layouts. In those situations, you may have to add many breakpoint utility classes and still not arrive at the most ideal presentation.

Or, developers may be using certain CSS grid and flex behaviors to approximate container responsiveness. But flex and CSS grid solutions are restricted to only loosely defining layout adjustments from horizontal to vertical arrangements, and don’t address the need to modify other properties.

Container queries move us beyond considering only the viewport, and allow any component or element to respond to a defined container’s width. So while you may still use a responsive grid for overall page layout, a component within that grid can define its own changes in behavior by querying its container. Then, it can adjust its styles depending on whether it’s displayed in a narrow or wide container.

We also kept main as a container. This means we can add styles for the .article class, but they will be in response to the width of main, not themselves. I am anticipating this ability to have rules within container queries responding to multiple layers of containers cause the most confusion for initial implementation and later evaluation and sharing of stylesheets.

In the near future, updates to browser's DevTools will certainly help in making DOM changes that alter these types of relationships between elements and the containers they may query. Perhaps an emerging best practice will be to only query one level up within a given @container block, and to enforce children carrying their container with them to reduce the potential of negative impact here. The trade-off is the possibility of more DOM elements as we saw with our article example, and consequently dirtying semantics.

In this example, we saw what happened with both nested containers and also the effects of introducing flex or grid layout into a container. What is currently unsettled in the spec is what happens when a container query is defined but there are no actual container ancestors for those queried elements. It may be decided to consider containment to be false and drop the rules, or they may fallback to the viewport. You can track the open issue for fallback containment and even add your opinion to this discussion!

Container Element Selector Rules

Earlier I mentioned that a container cannot itself be styled within a container query (unless it’s a nested container and responding to its ancestor container’s query). However, a container can be used as part of the CSS selector for its children.

Why is this important? It allows retaining access to CSS pseudo-classes and selectors that may need to originate on the container, such as :nth-child.

Given our article example, if we wanted to add a border to every odd article, we can write the following:

@container (min-width: 60ch) {
  .container:nth-child(odd) > article {
    border: 1px solid grey;
  }
} 

If you need to do this, you may want to use less generic container class names to be able to identify in a more readable way which containers are being queried for the rule.

Case Study: Upgrading Smashing Magazine’s Article Teasers

If you visit an author’s profile here on Smashing (such as mine) and resize your browser, you’ll notice the arrangement of the article teaser elements change depending on the viewport width.

On the smallest viewports, the avatar and author’s name are stacked above the headline, and the reading time and comment stats are slotted between the headline and article teaser content. On slightly larger viewports, the avatar floats left of all the content, causing the headline to also sit closer to the author’s name. Finally, on the largest viewports, the article is allowed to span nearly the full page width and the reading time and comment stats change their position to float to the right of the article content and below the headline.

By combining container queries with an upgrade to using CSS grid template areas, we can update this component to be responsive to containers instead of the viewport. We’ll start with the narrow view, which also means that browsers that do not support container queries will use that layout.

Now for this demo, I’ve brought the minimum necessary existing styles from Smashing, and only made one modification to the existing DOM which was to move the headline into the header component (and make it an h2).

Here’s a reduced snippet of the article DOM structure to show the elements we’re concerned about re-arranging (original class names retained):

<article class="article--post">
  <header>
    <div class="article--post__image"></div>
    <span class="article--post__author-name"></span>
    <h2 class="article--post__title"></h2>
  </header>
  <footer class="article--post__stats"></footer>
  <div class="article--post__content"></div>
</article>

We’ll assume these are direct children of main and define main as our container:

main {
  contain: layout inline-size style;
}

In the smallest container, we have the three sections stacked: header, stats, and content. Essentially, they are appearing in the default block layout in DOM order. But we’ll go ahead and assign a grid template and each of the relevant elements because the template is key to our adjustments within the container queries.

.article--post {
  display: grid;
  grid-template-areas: 
    "header" 
    "stats" 
    "content";
  gap: 0.5rem;
}

.article--post header {
  grid-area: header;
}

.article--post__stats {
  grid-area: stats;
}

.article--post__content {
  grid-area: content;
}

Grid is ideal for this job because being able to define named template areas makes it much easier to apply changes to the arrangement. Plus, its actual layout algorithm is more ideal than flexbox for how we want to manage to resize the areas, which may become more clear as we add in the container query updates.

Before we continue, we also need to create a grid template for the header to be able to move around the avatar, author’s name, and headline.

We’ll add onto the rule for .article`--post header`:

.article--post header {
  display: grid;
  grid-template-areas:
    "avatar name"
    "headline headline";
  grid-auto-columns: auto 1fr;
  align-items: center;
  column-gap: 1rem;
  row-gap: 0.5rem;
}

If you’re less familiar with grid-template-areas, what we’re doing here is ensuring that the top row has one column for the avatar and one for the name. Then, on the second row, we’re planning to have the headline span both of those columns, which we define by using the same name twice.

Importantly, we also define the grid-auto-columns to override the default behavior where each column takes up 1fr or an equal part of the shared space. This is because we want the first column to only be as wide as the avatar, and allow the name to occupy the remaining space.

Now we need to be sure to explicitly place the related elements into those areas:

.article--post__image {
  grid-area: avatar;
}

.article--post__author-name {
  grid-area: name;
}

.article--post__title {
  grid-area: headline;
  font-size: 1.5rem;
}

We’re also defining a font-size for the title, which we’ll increase as the container width increases.

Finally, we will use flex to arrange the stats list horizontally, which will be the arrangement until the largest container size:

.article--post__stats ul {
  display: flex;
  gap: 1rem;
  margin: 0;
}

Note: Safari recently completed support of gap for flexbox, meaning it’s supported for all modern browsers now! 🎉

We can now move to our first of two container queries to create the midsize view. Let’s take advantage of being able to create font-relative queries, and base it on the container exceeding 60ch. In my opinion, making content things relative to line length is a practical way to manage changes across container widths. However, you could certainly use pixels, rems, ems, and possibly more options in the future.

For this middle size, we need to adjust both the header and overall article grid templates:

@container (min-width: 60ch) {
  .article--post header {
    grid-template-areas:
      "avatar name"
      "avatar headline";
    align-items: start;
  }

  .article--post {
    grid-template-areas: "header header" ". stats" ". content";
    grid-auto-columns: 5rem 1fr;
    column-gap: 1rem;
  }

  .article--post__title {
    font-size: 1.75rem;
  }
}

At this width, we want the avatar to appear pulled into its own column to the left of the rest of the content. To achieve this, within the header the grid template assigns it to the first column of both rows, and then shifts the name to row one, column two, and the headline to row two, column two. The avatar also needs to be aligned to the top now, so adjust that with align-items: start.

Next, we updated the article grid template so that the header takes up both columns in the top row. Following that, we use the . character to assign an unnamed grid area for the first column of the second and third row, preparing for the visual of the avatar appearing in its own column. Then, we adjust the auto columns to make make the first column equivalent to the avatar width to complete the effect.

For the largest container size, we need to move the stats list to appear on the right of the article content, but below the headline. We’ll again use ch units for our “breakpoint”, this time picking 100ch.

@container (min-width: 100ch) {
  .article--post {
    grid-template-areas: "header header header" ". content stats";
    grid-auto-columns: 5rem fit-content(70ch) auto;
  }

  .article--post__stats ul {
    flex-direction: column;
  }

  .article--post__title {
    max-width: 80ch;
    font-size: 2rem;
  }

  .article--post__content {
    padding-right: 2em;
  }
}

To meet all of our requirements, we now need to take care of three columns, which makes the first row a triple repeat of header. Then, the second row starts with an unnamed column shared with content and then stats.

Looking at the real version of this page, we see that the article doesn’t span 100% of the width of Smashing’s layout. To retain this cap, within the grid-auto-columns we’re using the fit-content function, which can be read as: “grow up until intrinsic max-width of the content, but no greater than the provided value”. So, we’re saying the column can grow but not exceed 70ch. This does not prevent it from shrinking, so the column remains responsive to its available space as well.

Following the content column, we define auto for the stats column width. This means it will be allowed to take the inline space it needs to fit its content.

Now, you may be thinking that we’ve kind of just done media queries but in a slightly different way. Which — if we pause a moment — is kind of great! It should help container queries feel familiar and make them easier to adjust to and include in your workflow. For our demo, it also currently feels that way because our single article is currently responding to one parent element which itself is only responding to the changing viewport.

What we’re really done is set the foundation for this article to be dropped in on an article page, or on the home page where the articles are arranged in columns (for the sake of this demo, we’ll ignore the other changes that happen on the home page). As we learned in the intro example, if we want elements to respond to the width of CSS grid tracks or flex items, we need to have them carry their container with them. So let’s add an explicit container element around each article instead of relying on main.

<div class="article--post-container">
    <article class="article--post"></article>
</div>

Then, we’ll assign .article`--post-container` as a container:

.article--post-container {
  contain: layout inline-size;
}

Now, if we create a flex-based grid layout as we did in the intro example, we’ll place one article by itself above that grid, and two within the flex grid. This results in the following adjustments as the containers change size:

The video helps demonstrate the changes that are now able to happen completely independently of viewport-based media queries! This is what makes container queries exciting, and why they’ve been sought after by CSS developers for so long.

Here is the full CodePen of this demo including the flex grid:

See the Pen Container Queries Case Study: Smashing Magazine Article Excerpts by Stephanie Eckles.

Opportunities and Cautions for Using Container Queries

You can start preparing to use container queries today by including them as a progressive enhancement. By defining styles that work well without container queries, you can layer up enhancements that do use them. Then, unsupporting browsers will still receive a workable — if less than ideal — version.

As we look towards the future of being able to use container queries anywhere, here are some possible opportunities where they will be beneficial, as well as some cautions. All of them share one trait: they are scenarios when it’s likely to be considered desirable that layout and style changes will be independent from the viewport.

Responsive Typography

You may be familiar with the concept of responsive or fluid typography. Solutions for making typography update across viewport and element widths have seen many advancements, from JavaScript assisting, to CSS solutions using clamp() and viewport units.

If the spec receives a container unit (which we’ll talk about shortly), we may be able to achieve intrinsic typography. But even with the current spec, we can define responsive typography by changing the font-size value across various sizes of contained elements. In fact, we just did this in the example of the Smashing Magazine articles.

While this is exciting from a design and layout point of view, it comes with the same caution as existing fluid typography solutions. For accessibility, a user should be able to zoom the layout and increase font-size to 200% of its original size. If you create a solution that drastically shrinks font size in smaller containers — which may be the computed size upon zoom — a user may never be able to achieve increasing the base font-size by 200%. I’m sure we will see more guidelines and solutions around this as we all get more familiar with container queries!

Changing Display Values

With container queries, we’ll be able to completely change display properties, such as from grid to flex. Or change their related properties, like update grid templates. This makes way for smoothly repositioning child elements based on the current space allowed to the container.

This is the category you’ll find many of the current demos fall into, as it seems to be one of the things that makes the possibility of container queries so exciting. Whereas responsive grid systems are based on media queries tied to viewport width, you may find yourself adding layers of utility classes to get the result you’re really after. But with container queries, you can exactly specify not only a grid system, but completely change it as the element or component grows and shrinks.

Possible scenarios include:

  • changing a newsletter subscription form from horizontal to stacked layout;
  • creating alternating grid template sections;
  • changing image aspect ratios and their position versus related content;
  • dynamic contact cards that reposition avatars and contact details and can be dropped in a sidebar just as easily as a page-width section

Here it should be noted that just as in our pre-container query world, for accessibility it's advised to ensure a logical order especially for the sake of tabbing interactive elements like links, buttons, and form elements.

Showing, Hiding And Rearranging

For more complex components, container queries can step in and manage variations. Consider a navigation menu that includes a series of links, and when the container is reduced, some of those links should hide and a dropdown should appear.

Container queries can be used to watch sections of the navigation bar and change those individual parts independently. Contrast this to trying to handle this with media queries, where you might opt to design and develop against breakpoints with the result of a compromised, less ideal final solution.

Develop Once, Deploy Anywhere

Okay, this might be a bit aspirational. But for design systems, component libraries, and framework developers, container queries will greatly improve the ability to deliver self-defensive solutions. Components' ability to manage themselves within any given space will reduce complications introduced when it comes time to actually add them into a layout.

At first thought, this seems like the dream, especially if you’ve been involved with design system development as I have. However, the pros and cons may be equal for some components, and you may not always want the dynamic layout or repositioning behavior. I anticipate this will be another area best practices will form to perhaps have components opt-in to using container query behavior per instance, such as through a modifier class.

For example, consider a card component that assumes that font-size should change. But, in a particular instance, the actual character counts were much longer or much shorter and those rules were suboptimal. An opt-in would likely be easier than trying to override every single container query that was attached.

What Might Change in the Spec

Currently, even the syntax is subject to change before the spec is fully finalized. In fact, it’s been released as an experiment so that as a community we can provide feedback. Miriam Suzanne has created a GitHub project to track issues, and you may react to those and add comments.

I already mentioned two key issues yet to be resolved:

  • #6178: How does @container resolve when no ancestor containers have been defined?
  • #5989: What container features can be queried?

Of high importance and impact is:

  • Should there be a new syntax for establishing queryable containers? (#6174) The initial issue from Miriam proposes two options of either a new set of dedicated contain values or an entirely new property perhaps called containment. If any of these changes come through in the prototype phase, the values demonstrated at the start of this article may no longer work. So once again, note that it’s great to experiment, but be aware that things will continue changing!

Also of interest is the possibility of new container units, tracked in:

  • ["container width" and "container height" units (#5888)
    This could open up a native CSS solution for intrinsic typography among other things, something like: font-size: clamp(1.5rem, 1rem + 4cw, 3rem) (where cw is a placeholder for a currently undefined unit that might represent container width).

Additional Demos And Resources

  • Review Miriam’s CodePen collection where she is gathering container queries created on that platform
  • Ahmad Shadeed created an overview with practical examples
  • Andy Bell considered how to incorporate container queries as a progressive enhancement for card components
  • Stu Robson has created a GitHub repository to collect resources called Awesome-Container-Queries
  • David A. Herron wrote a quick start guide with a demonstration of elements that change at different rates based on container queries
  • A page has also been started about container queries on MDN.