Introduction

Ah, container queries - a topic I explored three years ago. It’s truly satisfying to revisit it once more, this time with a deeper and more comprehensive approach.

In this interactive guide to container queries, I will explain the problems they solve, how they work, and how we can use them today in our workflows.

What is the problem CSS container queries solve?

While building layouts in CSS, we always wanted a way to change a specific UI based on the width of its container, not the viewport. As a result, we tend to use media queries for stuff that is related to the container, not the viewport.

In the following example, we have a page header that contains:

  • Order number
  • Actions: search and filter

When there is no space, I want to hide the label from the search and filter actions. In media queries, we can do it like so:

@media (min-width: 580px) {
  /* Show the search and filter labels only *
  * if the screen width is 580px or more. */
  .search {
    width: 32px;
    height: 32px;
  }

  .search-label,
  .filter-label {
    display: block;
  }
}

Try to resize the browser below and notice how the search and filter label is hidden when the viewport width is less than 580px.

Logo

Order #2301

A little desc here.

Search
Filter

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Recent orders

The downside of using a media query is that the container will get bigger when the sidebar is hidden. In this case, it’s more logical to show the search and filter in their larger size.

See the following, there is enough space, but the search and filter are collapsed due to the use of a media query.

Logo

Order #2301

A little desc here.

Search
Filter

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Recent orders

Using CSS container queries

When using a container query, we can fine-tune such details and give the components the ability to expand based on their container width, not the viewport.

To use container queries, we need to define the container type. The container name is optional.

.main {
  container-name: main;
  container-type: inline-size;
}

Once defined, we can use the container query just like a media query. Here, I’m showing the <span> only if the width is 370px or more.

@container main (min-width: 370px) {
  .search-label,
  .filter-label {
    display: block;
  }
}

First, we need to define a container in the main section. Second, we can use @container with the child elements we need (search and filter actions).

Play with it yourself in the following demo by resizing the browser window.

Logo

Order #2301

A little desc here.

Search
Filter

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Recent orders

@media (min-width: 580px) {
.search-label,
.filter-label {
display: block;
}
.search {
flex: 1;
}
}
@container card (min-width: 200px) {
.search-label,
.filter-label {
display: block;
}
.search {
flex: 1;
}
}

Interesting, right? Let’s explore another example to make sure the problem container queries solve is clear.

The card component

The problem

We have a list of cards that change based on the viewport width. To make it change based on the viewport size, I used CSS grid minmax() function.

.layout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 1rem;
}

Play with the demo (resize the browser window).

Logo

How to write CSS

New CSS features you should know

The first issue I see is that the cards are too big when the viewport size is large. Notice the ratio between the thumbnail and title sizes.

We can fix that by using auto-fill in the CSS grid minmax() function, but it will leave us with empty columns.

Logo

How to write CSS

New CSS features you should know

Not the best. Now we have empty columns. What if, we want to change the card layout to horizontal? We can do that.

@media (min-width: 800px) {
  .card {
    display: flex;
    align-items: start;
  }
}

Try to change the viewport size. The card will change from a stacked style to a horizontal one.

Logo

How to write CSS

New CSS features you should know

It works as expected. Now, when the viewport size is above 800px, the card style will change to horizontal.

But.. there is a problem! What if we added more cards? The horizontal style will look too condensed. This is not good.

Play with the following demo:

Logo

How to write CSS

New CSS features you should know

CSS Flexbox guide

Notice that the horizontal style became too condensed, this is because we used a media query. When using a media query, the browser doesn’t care about what will happen to the components if the available space isn’t enough.

The solution

CSS container queries allow us to query against the container of an element. In the context of the previous example, we can change the style to horizontal only if the card’s container is large enough.

Here is the card’s markup:

<div class="layout__item">
  <div class="card">
    <div class="card-thumb">
      <img src="thumb.jpg" alt="" />
    </div>
    <div class="card-content">
      <h3>Card title in here</h3>
    </div>
  </div>
</div>

In CSS, we need to define the container type. We can also define the name (not mandatory but recommended).

.layout__item {
  container-type: inline-size;
  container-name: card;
}

We can use it like this:

@container card (min-width: 180px) {
  .card {
    display: flex;
    gap: 1rem;
    outline: dotted 2px deeppink;
  }
}

In the following interactive demo, you can try the following:

  • Add more or remove cards.
  • Choose between media or container query.
  • Resize the browser window.
  • See the current CSS that is applied based on the selected query type.

Logo

How to write CSS

New CSS features

CSS flexbox guide

CSS grid guide

@media (min-width: 800px) {
.card {
display: flex;
}
}
@container card (min-width: 200px) {
.card {
display: flex;
}
}

As you’ve seen using a media query to change the card style isn’t logical in this case. The number of cards can increase or decrease and that will affect the card’s content.

CSS container queries will help us to solve that problem by providing us with a way to query against the container.

Container query syntax

The container type

The default value for container-type is normal. To use a container query, we need to use inline-size as a value.

.main {
  container-type: inline-size;
}

We can then query elements that are within the .main container, here is an example:

.section {
  @container (min-width: 700px) {
    /* Custom styling */
  }
}

Every container is a style container by default unless we change the container-type to inline-size.

The container name

What happens if we have more than one container, how do we target them? Well, that’s why we have container-name. We can name a container and call it when used.

.main {
  container-type: inline-size;
  container-name: main;
}

.section {
  @container main (min-width: 700px) {
    /* Custom styling */
  }
}

Size container queries shorthand

We can use the shorthand version to add the container name and type. It’s useful if you prefer an easier scanning of the name and type.

.main {
  container: main / inline-size;
}

Style queries syntax

CSS style container queries allow us to query a CSS variable and do custom styling based on that. It’s currently supported in Chrome and Safari Technology Preview only, but it’s worth mentioning and explaining in this guide.

By default, every element is a style query. To use it, we can query a CSS variable on a parent element and write custom CSS that will apply to its child elements.

.article-wrapper {
  --featured: true;
}

.article {
  @container style(--featured: true) {
    /* Custom CSS */
  }
}

I will share a few use cases later in the guide.

Now that you know what the problem container queries solve and how the syntax works, let’s move to their common pitfalls.

Common pitfalls for container queries

A container can’t be sized by its contents

The container size should either be explicit or come from a flex or a grid layout. What does that mean? Let me show you an example.

In the following demo, we have a website header with a navigation.

<div class="header">
  <nav class="nav">
    <ul>
      <!-- Navigation items -->
    </ul>
  </nav>
</div>

In CSS, I added the following:

.nav {
  container: nav / inline-size;
  display: flex;
  gap: 1rem;
  outline: solid 1px;
}

Notice how the outline is collapsed. When using the inline-size containment, this will prevent the container from getting size information from its content. As a result, it will collapse to zero.

Try to toggle the “Stretch nav” button to see the fix.

Logo

HomeProjectsUsersSettingsProfile

To fix that, we need to force the element to fill the available space in header.

.nav {
  flex: 1;
}

It’s not possible to query a container against itself

Let’s suppose that we have a size container on an element. At some point, we wanted to style the container by querying the container itself.

.main {
  container: main / inline-size;
}

@container main (min-width: 700px) {
  background-color: #fefefe;
}

The background-color property won’t apply. This is not possible.

The container should be an additional element

Since we can’t query a container against itself, the container should be in a separate element. In some cases, the additional element is already there.

<div class="o-grid">
  <div class="o-grid__item">
    <article class="c-article">
      <!-- content -->
    </article>
  </div>
</div>
.o-grid__item {
  container: card / inline-size;
}

.c-article {
  @container card (min-width: 500px) {
    /* Styling */
  }
}

Container query units

We used to use CSS viewport units to have a fluid typography, but the issue is, you guessed it, that it’s limited to the viewport only.

With CSS container queries, we can finally use units that are scoped to the container itself.

In the following example, I used query units to have a dynamic font size.

.author-name {
  font-size: clamp(14px, 10px + 1.33cqw, 20px);
}

font-size: clamp(14px, 10px + 1.33cqw, 20px);

Logo

Font size: 20

Ahmad Shadeed

Author of Debugging CSS

As you’ve seen, we can use query units for font sizes. However, the use cases aren’t limited to that only. We can use them for spacing (padding or margin) and more.

According to MDN, here are all the query units we can use:

  • cqw: 1% of a query container’s width
  • cqh: 1% of a query container’s height
  • cqi: 1% of a query container’s inline size
  • cqb: 1% of a query container’s block size
  • cqmin: The smaller value of either cqi or cqb
  • cqmax: The larger value of either cqi or cqb

For me, I care about the cqw unit. I will use them in the upcoming use-cases section.

Container queries use cases

Container query demos are supported in all major browsers (Chrome 106+, Safari 16.0+, Firefox 110+).

News section

One of the most common use cases for container queries is the card component. That can become handy when building featured news sections, for example.

In the following example, we have a news section with two main sections:

  • Left section: featured article with three articles underneath
  • Right section: three articles without a thumb

We can use container queries for that:

  • Featured article
  • Switching from a stacked to a horizontal card and vice versa

With that, we can create a new component that adapts nicely to the changes in its wrapper width.

See the following figure:

To make it more clear, I created a simulation for breakpoints that you can see in the following interactive demo.

Play with the next and previous buttons, or click on the rectangles and see the explanation.

Viewport width: 700

Default view.

Logo

Card title in here
Card title in here
Card title in here
Card title in here

A bit of desc text in here. A bit of desc text in here.

Card title in here

A bit of desc text in here. A bit of desc text in here.

Card title in here

A bit of desc text in here. A bit of desc text in here.

Here is a video for you that shows the demo above, just in case.

As you’ve seen, there are seven different breakpoints for this section. If we want to use a media query for this, things can get dirty quickly.

@media (min-width: 411px) {
  .card {
    /* Horizontal style */
    flex-direction: row;
    gap: 1rem;
  }
}

@media (min-width: 431px) {
  .card--featured {
    /* Horizontal style */
    flex-direction: row;
    gap: 1rem;
  }

  .card--featured .card-thumb {
    /* Make it larger */
  }
}

@media (min-width: 499px) {
  .card--featured {
    /* Horizontal style */
    flex-direction: row;
    gap: 1rem;
  }

  .cards-list .card {
    /* Stacked style */
  }
}

@media (min-width: 554px) {
  .card--featured .card-thumb {
    /* Make it smaller */
  }

  .cards-list .card {
    /* Horizontal style */
  }
}

@media (min-width: 581px) {
  .card--featured .card-thumb {
    /* Make it larger */
  }
}

@media (min-width: 700px) {
  .cards-list .card {
    /* Stacked style */
  }
}

This is complex and I didn’t even write all the CSS, I just added a pseudo-style code. With container queries, this is fairly easy. Let’s see how!

The card component

The card component has two states: stacked and horizontal. It will become horizontal if its container is wider by a specific number.

First, the card should be wrapped in a container:

<div class="card-wrapper">
  <div class="card">
    <!-- Card content -->
  </div>
</div>

Then, in CSS we write the following.

.card-wrapper {
  container: card / inline-size;
}

.card {
  /* Default styles */
  display: flex;
  flex-direction: column;
  gap: 1rem;

  @container card (min-width: 250px) {
    flex-direction: row;

    .card-thumb {
      flex: 0 0 calc(2cqw + 80px);
    }
  }
}

As simple as that. We can add a few spices in CSS, too.

For example, I used a query unit in the calc() function for the card thumbnail. This is equal to “2 container query width”, which is 2% of the container’s width.

Card title in here

A bit of desc text in here.

Card title in here

A bit of desc text in here.

Here is the card component in an isolated demo. Try to resize the handle in between the two variations.

This is a variation of the card but with a larger thumbnail and font sizes. I added the class card--featured to style it.

<div class="card-wrapper">
  <div class="card card--featured">
    <!-- Card content -->
  </div>
</div>

In CSS:

.card--featured .card-thumb {
  @container card (min-width: 400px) {
    .cardThumb {
      flex: 0 0 calc(10cqw + 200px);
    }
  }
}

It’s worth mentioning that in style container queries, we can add a CSS variable instead of a CSS class.

<div class="card-wrapper">
  <div class="card " style="--featured: true;">
    <!-- Card content -->
  </div>
</div>
@container card (min-width: 400px) and style(--featured: true) {
  .cardThumb {
    flex: 0 0 calc(10cqw + 200px);
  }
}

At the time of writing this guide, style queries work in Google Chrome and are supported by default in Safari Technology Preview.

Here is the final demo below. You can try the following:

  • Try the “Auto resize” button and see the window grow.
  • Use the range slider.
  • Remove a card and see what happens.
Viewport width: 700

Logo

Card title in here
Card title in here
Card title in here
Card title in here

A bit of desc text in here. A bit of desc text in here.

Card title in here

A bit of desc text in here. A bit of desc text in here.

Card title in here

A bit of desc text in here. A bit of desc text in here.

Steps component

In this example, we have a steps component that needs to look differently based on its container size. If the container is wide, each step will look like an arrow. If not, they will stack on top of each other.

Resize the window below and see how the component changes.

Viewport width: 700

Logo

Overview

  1. Intro
  2. How it works
  3. Review

Overview

In order to finish the guide, you need to go through the following steps.

  1. Intro
  2. How it works
  3. Review

Container queries make that straightforward.

.content {
  container: steps / inline-size;
}

.steps__item {
  @container steps (min-width: 450px) {
    padding: 12px 8px 12px 18px;

    &:before {
      --size: 2rem;
      font-size: 16px;
    }

    &:first-child {
      clip-path: polygon(/* path */);
    }

    /* And so on.. */
  }
}

Social feed

I spotted this pattern on Linkedin. When the viewport size is small, the action buttons label is hidden on smaller sizes. It works but is not ideal.

With media queries, this can be done but it will still be a hack.

.action-label {
  display: none;
}

@media (min-width: 500px) {
  .action-label {
    display: inline-flex;
  }
}

This will work, but the simplest change to the markup or the UI can cause it to fail. The reason is obvious because we’re querying the viewport.

See the following demo:

Viewport width: 700

People you may know

😃The Happy
🤩The Excited
🧐The Curious

Ahmad Shadeed

Author of Debugging CSS

Ahmad Shadeed

Author of Debugging CSS

When the viewport width is less than 630px, the label is hidden.

However, in the following case, the sidebar is hidden and the feed wrapper gets wider, but the label is still hidden.

Viewport width: 450

People you may know

😃The Happy
🤩The Excited
🧐The Curious

Ahmad Shadeed

Author of Debugging CSS

Ahmad Shadeed

Author of Debugging CSS

This is where container queries shine.

.action-label {
  @container content (min-width: 470px) {
    display: block;
  }
}

Here is the demo with container queries. Notice the state in the top left corner and see how it changes based on the container’s width. It’s magic, isn’t it?

Container width > 470px

People you may know

😃The Happy
🤩The Excited
🧐The Curious

Ahmad Shadeed

Author of Debugging CSS

Ahmad Shadeed

Author of Debugging CSS

Dashboard widgets: Example 1

Oh, dashboard widgets. I guess this is a top-use example for CSS container queries. In a dashboard, the user might have the ability to customize the width or the size of the columns.

In the following example, all the widgets are the same component but they change differently based on their container width.

Try to toggle between the grid and see it yourself.

Orders Status

New orders

44

Processing

34

Delivered

4.323

Shipment Overview

Shipped

44

Delayed

34

Shipment Overview

Shipped

44

Delayed

34

Cool, right? There is no magic in here. I just wrote three different styles for the same widget using @container.

.widget {
  /* Default styling - Style 1 */
  display: flex;
  align-items: center;
  justify-content: space-between;

  /* Style 2 */
  @container column (max-width: 150px) {
    flex-direction: column;
    gap: 4px;
    background-color: #fff;
    padding: 5px;
    border-radius: 8px;
  }

  /* Style 3 */
  @container column (min-width: 250px) {
    flex-direction: column;
    align-items: start;

    .icon {
      display: block;
      /* Other styles */
    }
  }
}

Feel free to inspect the demo above in case you want to see all the CSS details.

Dashboard widgets: Example 2

In this example, we have a list of stats that represent some data from a system. If a stats item got a wider container, I want to change its design.

Logo

Total sales

405

Number of users

5253

Last hour

52

Returns

5

Recent order

#0101

Here is the CSS:

.widget {
  @container column (min-width: 280px) {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    background-color: #fff;

    h3 {
      font-size: 1.5rem;
    }

    p {
      font-size: 1.2rem;
    }
  }
}

Dashboard widgets: Example 3

Another example in a dashboard is a progress component. The component should adapt based on its container width.

Try to toggle between “Grid 1” and “Grid 2”.

Shipment Progress

Shipped

64%

Delayed

34%

Shipment Progress

Shipped

88%

Delayed

75%
.progress {
  /* Default styles */

  @container column (min-width: 250px) {
    flex-direction: column;
    align-items: start;
    gap: 0.5rem;

    .percentage {
      display: block;
    }

    .progressWrapper {
      flex: auto;
      width: 100%;
      max-width: none;
    }

    .progress {
      height: 10px;
    }

    .percentage {
      font-size: 16px;
    }
  }
}

Options list

In this example, we have a list of options that the user can choose from. By default, both the icon and the label are in a new line. If there is enough space, the icon and label will be next to each other.

Play with the demo below:

  • Try to remove or add items (max is 5)
  • Check the “Auto resize” button or the range slider

Logo

What's your favorite meal?

Feel free to select multiple items.

Here is the CSS. As simple as that.

.option-wrapper {
  container: option / inline-size;
}

.option {
  @container option (min-width: 150px) {
    flex-direction: row;
    align-items: center;
  }
}

You might be considering using flex-wrap. While it will work, we can’t control exactly when it will wrap, resulting in some items being wrapped while others are not.

Form inputs

In this example, the form inputs have a custom grid layout that should be added if there is enough space. Without container queries, this isn’t possible unless we write multiple media queries.

See the breakpoints by clicking on the previous and next buttons.

Default view.

Logo

CSS is awesome.

Here is the CSS needed. By default, we have a 1-col grid. If the container width is more than 250px, the grid is changed.

.start {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;

  @container section (min-width: 250px) {
    grid-template-columns: 1.55fr 1fr;
  }
}

Here is a video of the previous demo, in case you’re on mobile:

Threads list

In this example, we have a thread component that needs to show differently based on the container size. We can’t do it with media queries as it won’t work for all the cases.

See the following figure:

To achieve that, we can use container queries as below:

  • I used query units and CSS clamp to handle the dynamic font size
  • I changed the flex-direction to row when there was enough space
.threadTitle {
  font-size: clamp(14px, 1cqw + 12px, 16px);
  font-weight: bold;
}

.threadMeta {
  font-size: clamp(13px, 1cqw + 11px, 15px);
  color: #5b5b5b;
}

.thread {
  @container thread (min-width: 320px) {
    flex-direction: row;
    align-items: flex-start;
  }
}

Each query unit represents 1% of the container width.

Here is the final demo:

Logo

Featured threads

How do I master CSS Grid Layouts?

Ahmad Shadeed3d

10

Can someone explain CSS Flexbox mysteries?

Samantha Lee7d

10

What are some advanced CSS techniques to transform designs?

Ethan Patel5d

10

Latest threads

How can I improve my CSS animations?

Emily Johnson4d

10

Where can I find CSS tutorials for beginners?

David Smith6d

10

What's the best approach to organize CSS files?

Alexandra Brown2d

10

Newsletter

A newsletter can be displayed in a small or a large space. We can accommodate that using container queries.

<div class="newsletter-wrapper">
  <div class="newsletter">
    <!-- Heading and form -->
  </div>
</div>
@container newsletter (inline-size < 250px) {
  h2 {
    /* The blue line */
    &:before {
      content: "";
      display: block;
      height: 4px;
      width: 60px;
      background-color: var(--brand-1);
    }
  }
}

/* If the container width is smaller than 250, move the button out of the flow and show an icon. */
@container newsletter (inline-size < 250px) {
  .form-group {
    position: relative;

    input {
      padding-right: 32px;
    }

    button {
      position: absolute;
      right: 4px;
      top: 8px;
      /* Other styles */
    }

    svg {
      display: block;
    }
  }
}

In the following demo, the newsletter has two variations based on the container width. Try to resize the handle between the two variations.

Subscribe to our newsletter

Get the latest tech news.

Subscribe to our newsletter

Get the latest tech news.

Timeline

In this example, the timeline component changes its layout based on the available space. The timeline can work in narrow and wide containers.

What you see below is the same component in different containers. On the left, we have the simple version. On the right, is the full version.

Try to resize the demo below.

⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

Featured threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

Latest threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

Have you noticed another in-between variation? I created three variations for this component to work with different containers.

Here is a simplified CSS:

@container timeline (inline-size > 300px) {
  .c-timeline__item {
    .c-timeline__content {
      padding-bottom: 3rem;
    }

    .c-timeline__content:after {
      left: calc(var(--size) / 2 * -1);
      width: var(--size);
      height: var(--size);
    }
  }
}

@container timeline (inline-size > 430px) {
  .c-timeline {
    display: flex;
    justify-content: center;
  }

  .c-timeline__item {
    --size: 40px;
    gap: 0;
    flex: 1;
    padding-left: 0;
    flex-direction: column;
    align-items: center;
  }
}

/* And a lot more CSS. Feel free to inspect the element in case you want to see the full CSS. */

A common pattern on the web is to expand or collapse a sidebar via a toggle button. We can use container queries to change the layout of the sidebar items.

See the following example and try to collapse the sidebar.

Logo

My profile

This is mostly done with CSS container queries and Javascript was used to change the sidebar width.

.aside {
  container: sidebar / inline-size;
}

.aside.collapsed {
  width: 50px;
}

.nav-item .label {
  opacity: 0;
  transform: translateX(-5px);
  transition: 0.3s ease-out;

  @container sidebar (min-width: 100px) {
    opacity: 1;
    transform: translateX(0);
  }
}

This might not be the perfect usage for complex sidebars, but it works well for this simple usage.

Event card

While checking TechCrunch, I noticed their implementation for the event card. It’s buggy and weird looking so I thought about why not giving it a chance with container queries.

Here is a video of their design:

As you see, there are a couple of issues:

  • Inconsistent size for the buttons.
  • Misaligned content (e.g.: event title, location).
  • The UI wraps for items with long content, making the others look odd.
  • No gap between content. At certain breakpoints, the content is very close to each other.

I honestly don’t know how this is still on TechCrunch. It’s full of bugs!

Let’s look at the HTML.

<div class="event-card">
  <div class="meta">
    <p>Apr 25, 2024</p>
    <p>TechCrunch Early Stage 2024</p>
    <p>Boston</p>
  </div>
  <div class="actions">
    <a href="#" class="btn"> Buy Tickets </a>
    <a href="#" class="btn ghost"> Be a sponsor </a>
  </div>
</div>

Each event card is wrapped inside an <li> which will be used as a container for each card.

<ul>
  <li class="card-wrapper">
    <div class="event-card">
      <!-- Event card -->
    </div>
  </li>
  <li class="card-wrapper"></li>
  <!-- And so on.. -->
</ul>

For the basic styling, the elements are stacked like the following example:

Logo

We can use flexbox dynamic wrapping to show the buttons next to each other when there is enough space.

.btn {
  flex: 1 0 110px;
}

That means each button’s minimum width is 110px. If there is not enough space, it will grow to fill its container.

In the following variation, the viewport width is slightly bigger, and as a result, the buttons are displayed next to each other.

Logo

Next, I used @container to check if the container size is larger than a specific value. If yes, the card’s content will be displayed next to each other.

.card-wrapper {
  container: event / inline-size;
}

@container event (min-width: 430px) {
  .event-card {
    display: flex;
    align-items: center;
    gap: 1rem;
  }

  .meta {
    flex: 1;
  }
}

Logo

Cool. The next variation is the final one, where all the content is on the same row. Think of it as a table style. I did the following:

  • Changed the .meta to flex-direction: row
  • The date and location have a fixed width.
  • The title is fluid to the available space
  • Reset the flex property for the buttons
@container event (min-width: 620px) {
  .meta {
    flex-direction: row;
    gap: 1.5rem;

    .title {
      flex: 1;
    }

    .date,
    .location {
      flex: 0 0 100px;
    }
  }

  .btn {
    flex: initial;
  }
}

See the result below and try to resize the window or play with the auto-resize button.

Logo

The story doesn’t end here. We used container queries to build the card component and now it works regardless of its container size.

Github repo header

In Github, the header section in the repository page contains buttons and menus that change based on the viewport width.

See the following video.

If you look at 0:11, notice how the aside is hidden and there is more space, but some of the details are hidden. In this specific case, container queries can fix that.

I tried to recreate a mockup of the Github header so you can play with it yourself. Here is the demo:

Logo

main
1Branch
0Tags
Go to file
Add file

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Info

item 1

item 223

item dksd

item dsod sk

item dsdkso

When the aside is hidden, the UI still has the same view even though their container is now bigger.

Logo

main
1Branch
0Tags
Go to file
Add file

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Info

item 1

item 223

item dksd

item dsod sk

item dsdkso

Here is the CSS for the prototype above.

.meta {
  @container main (min-width: 330px) {
    display: flex;
  }
}

.menuButton {
  &.add {
    @container main (min-width: 330px) {
      display: flex;
    }

    @container main (max-width: 539px) {
      svg:first-of-type,
      span {
        display: none;
      }
    }

    @container main (min-width: 540px) {
      svg:last-of-type {
        display: none;
      }
    }
  }

  &.dots {
    @container main (min-width: 330px) {
      display: none;
    }
  }
}

.metaItem {
  span {
    @container main (min-width: 540px) {
      display: block;
    }
  }
}

.search {
  @container main (min-width: 330px) {
    display: flex;
  }

  @container main (max-width: 539px) {
    padding: 4px 10px;
    background: #f6f8fa;
    border: 1px solid #d0d7de;
    box-shadow:
      0px 1px 0px rgba(31, 35, 40, 0.04),
      inset 0px 1px 0px 1px rgba(255, 255, 255, 0.25);

    svg {
      display: none;
    }
  }

  @container main (min-width: 540px) {
    min-width: 160px;
    background: #fff;
    border: 2px solid #ececec;
  }
}

Logo

main
1Branch
0Tags
Go to file
Add file

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

card title

card desc in here

Info

item 1

item 223

item dksd

item dsod sk

item dsdkso

Style queries use cases

Please keep in mind that style queries are supported only in Google Chrome and Safari Technology Preview at the time of writing this guide.

Dynamic timeline layout

I explored an example in the container queries where we have a timeline that changes based on the container size. It works, but there is one problem. When the number of timeline items is 4 or more, the full variation will become weird-looking.

See the demo below:

Logo

Featured threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

Latest threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

  4. Code Review

  5. Team Standup

  6. Meeting with Client

How we can fix that? We can check with CSS :has() if the number of timeline items is 5 or more. If yes, we set a CSS variable --force-vertical: true.

.timelineWrapper {
  container: timeline / inline-size;
  --force-vertical: false;

  &:has(.c-timeline__item:nth-last-child(n + 5)) {
    --force-vertical: true;
  }
}

Then, we can combine the size and style queries together to show the full variation only if the number of items is less than 5 and have the minimum size needed.

@container timeline (inline-size > 430px) and style(--force-vertical: false) {
  /* Apply the full variation. */
}

Test it out below. Try to add more items and see what happens. This demo works on the latest Chrome.

⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

Featured threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

Latest threads

  1. Meeting with Client

  2. Presentation

  3. Project Kickoff

If the browser you’re using doesn’t support style queries, see the video below.

The fluid blockquote

A blockquote can live in different places on a web page. For example, it can be in a sidebar, article body, full-width section, and more.

We can change its style based on the container width, but that might not be enough. We need to style it based on the context, too.

To do that, we can set a custom CSS variable on the outer container.

<div class="content" style="--context: article">
  <blockquote>
    <!-- Blockquote content here -->
  </blockquote>
</div>

And in CSS, we can do the following:

@container style(--context: article) {
  blockquote {
    /* Custom CSS */
  }
}

Let’s take a closer look.

Article variation

This style is boxed, centered and designed to catch the user’s eye.

@container style(--context: article) {
  .articleQuote {
    background-color: lightcyan;
    padding: 1rem 0.5rem;
    text-align: center;
    border-radius: 12px;
    max-width: 370px;
    margin: auto;

    &:before {
      /* Quote icon */
    }
  }
}
⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

CSS is awesome

some desc in here

A blockquote can live in different places on a web page. For example, it can be in a sidebar, article body, full-width section, and more.

CSS container queries is great. I recommend to give it a try. It's useful!

Ahmad Shadeed

A blockquote can live in different places on a web page. For example, it can be in a sidebar, article body, full-width section, and more.

We can change its style based on the container width, but that might not be enough. We need to style it based on the context, too.

Aside variation

A secondary content that should be catchy, a simple one.

@container style(--context: aside) {
  .articleQuote {
    p {
      font-size: 15px;
    }

    cite {
      font-weight: bold;
      font-size: 13px;
    }
  }
}
⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

CSS container queries is great. I recommend to give it a try. It's useful!

Ahmad Shadeed

Main news

This is a random paragraph just for the sake of this demo.

Full-width variation

A different background color and larger font size. This is an important quote and its style should reflect that.

.section:has(blockquote) {
  min-height: 180px;
  background-color: var(--brand-1);
}

@container style(--context: full) {
  .articleQuote {
    p,
    cite {
      color: #fff;
    }

    p {
      font-size: clamp(1rem, 1rem + 2cqw, 1.35rem);
    }
  }
}
⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

This is a section in a website.

CSS container queries is great. I recommend to give it a try. It's useful!

Ahmad Shadeed

This is a section in a website.

In case your browser doesn’t support style queries, here is a video for reference.

Conditional horizontal article

An example of where style queries can be useful is the ability to change a component based on the context, not only the container size.

See the following example. We have an aside with the section “Popular articles”. If there is a CSS variable --context: aside, I want the card to turn into the horizontal style if it’s within the size range.

⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

Main news

Homemade Lasagna Recipe

Easy Avocado Toast

Chocolate Lava Cake

Popular articles

Spicy Thai Green Curry

Summer Berry Salad

Hearty Beef Stew

<div class="section" style="--context: aside">
<div class="article">
<!-- -->
</div>
</div>

To do the above, we can nest the size query inside the style query. That way, the CSS will be applied only if

  • the container has the --context: aside
  • and the container size is between 170px and 485px
@container style(--context: aside) {
  @container (min-width: 170px) and (max-width: 485px) {
    .card {
      display: flex;
      align-items: center;
      gap: 0.5rem;

      img {
        aspect-ratio: 4 /3;
        flex: 0 0 clamp(60px, 60px + 10cqw, 120px);
      }
    }
  }
}

Toggle a style query from another

In this example, we have an article body with a section that contains related articles. I want to change the card style from stacked to horizontal only if it’s inside the related section.

⚠️

Your browser doesn't support CSS style queries. You can test the demo in Chrome or Safari Technology Preview.

Logo

A blockquote can live in different places on a web page. For example, it can be in a sidebar, article body, full-width section, and more.

CSS container queries is great. I recommend to give it a try. It's useful!

Ahmad Shadeed

Related articles

A blockquote can live in different places on a web page. For example, it can be in a sidebar, article body, full-width section, and more.

We can change its style based on the container width, but that might not be enough. We need to style it based on the context, too.

<div class="section" style="--context: related">
<div class="article">
<!-- -->
</div>
</div>

Here is the CSS:

  • First, we have the style query for the horizontal style. It only works if the --horizontal: true is added to the nearest parent.
  • Second, we check with another style query about the --context: related CSS variable. If it’s there, we switch the --horizontal: true one.
/* Horizontal style */
@container style(--horizontal: true) {
  .card {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    /* Other styles */

    img {
      flex: 0 0 clamp(60px, 60px + 10cqw, 120px);
    }
  }
}

/* Toggle the horizontal style if the cards
* are within the related section */
@container style(--context: related) {
  --horizontal: true;
}

Interesting, right? I found this usage very useful.

Container queries and DevTools

It’s worth mentioning that Chrome supports container queries in DevTools. In the following figure, I’m hovering over an element that is displayed via a container query.

I would have liked this more if the double arrow icon is properly aligned.

Resources

Outro

CSS container queries help us to write a truly fluid components that change based on their container size. In the next few years, we’ll see less media queries and more container queries. Of course, using media queries will still be useful for general layout stuff.

I hope you learned something new in the guide. Thank you for reading.

Enjoyed the read? If you'd like to support my work, consider buying me a coffee. Each article takes about 50 cups to create. Thanks a latte!

Support me on Ko-fi