Container queries and units in action

Published: October 23, 2025

One of the goals when writing CSS is to build component parts that will adapt well to different (and unexpected) contexts. Ideally, a component can be placed inside any "container" element without it feeling broken or out of place. How can you accomplish this in a complex layout like a store where the primary component—the "product"—has to fit into a variety of list layouts, including the sidebar?

Responsive typography with cqi units

The first step is to define some basic sizing variables that could be reused across the project—starting with whitespace sizes. But before creating any custom properties, the browser provides some useful named values as CSS units:

  • 1em: the current font size.
  • 1rem: the font size on the :root (html) element.
  • 1lh / 1rlh: the current and root line heights.
  • 1vw: the viewport width.
  • 1vi: the viewport "inline" size (for English, this is the same as vw).
  • 1cqi: the inline size of the nearest "container" (defaulting to the viewport).

You can think of these units as common variables provided by the browser, with a shorthand syntax for multiplication. If you wanted half of a --line-height custom property in CSS, you would need to write the entire calculation calc(0.5 * var(--line-height)), but with the lh unit, you can ask for 0.5lh instead.

Custom properties like --brand-color and --button-background have different meanings and serve a different purpose, even when they result in the same deepPink color (another browser-provided variable). Similarly, 1em might sometimes be equal to 16px, but that's not a stable relationship. Like any other variable, units should be used to express a relationship, rather than an expected value.

Both 1em and 1lh are font-relative units that could be used for spacing, but only one of them has a reliable relationship to the current line height. If this page involved a lot of elements with prose in them—paragraphs and lists, for example—the lh unit would work well for spacing between them:

p, ul, ol {
  margin-block: 1lh;
}

That will maintain a consistent baseline rhythm, without any extra work. But this page has almost no prose. Instead of spacing within a flow of text, this layout requires spacing to push text away from the edge of a card, and spacing between columns and rows in a grid or stacked layout. In this case there are several things to consider:

  • Multiples (or fractions) of 1lh may still be useful for maintaining vertical rhythm across the page.
  • The cqi unit would account for the amount of available space in a given context.

By combining these two units in a round() function, the --gap variable is based primarily on the container size, but rounded up to a multiple of quarter-lines:

html {
  --gap: round(up, 2cqi, 0.25lh);
}

If the line-height is 20px, then the --gap will be multiples of 5px—but the exact multiple will depend on available space. If either the line-height or available space change, the --gap variable will adapt to its new context. To make the --gap consistent across the entire design, you could replace the container-relative cqi units with viewport-relative vi units.

The same approach is useful when establishing "fluid" font sizes that respond to available space. This --body-text variable is based on the user-provided font preference, with some range to adapt based on the container. In this case, clamp() ensures the font will be at least as large as the user's preference, can grow some as the container size increases, but will stop growing at some point:

html {
  --body-text: clamp(1rem, 0.875rem + 0.5cqi, 1.25rem);
}
  • The range is clamped between 1rem and 1.25rem, to stay near the user-selected font size.
  • The cqi value determines how fast the font will grow or shrink in relation to available space, which is added to a rem value to offset that growth based on the user-selected size.

Keeping the central rem value near 1 and the added cqi value low ensures that users still have significant control when zooming in or out. You can adjust those two values, and then use browser zoom to see how they interact. The closer you get to 100cqi, and the farther you fall below 1rem, the less influence user font preferences will have—and the less font sizes will respond to zooming in or out.

The --item-title variable is slightly larger and more responsive, and the --list-title size responds to the viewport size (using vi) rather than the immediate container size. That way item headings respond to their context, but the main list headings all match in size no matter where they show up.

Defining containers to measure in context

At this point, container query units have been used to create adaptive typography on a web page, but new containers haven't been defined.

By default, 1cqi (1/100 container query inline size) is the same as 1svi (1/100 small viewport inline size) because the "small" viewport acts as the initial container for any web page. In order to take full advantage of the cqi unit, you need to define additional "containers" within the page. The primary layout containers on this page are the product-list and shopping-cart—so they are set to expose their inline-size.

product-list,
shopping-cart {
  container-type: inline-size;
}

Container query units—including cqi—aren't able to measure the element that they are used on. If you set the width of shopping-cart to 25cqi, it would be a paradox to determine the container's width based on its own width! Instead, the result will be based on the next ancestor container in the tree hierarchy that contains shopping-cart.

The product-detail cards are part of a product-list grid that can be changed by the user. The list layout option displays each card at full width. In that case, referring to the parent container size is useful, but both the small-grid and large-grid options cause the card to grow and shrink depending on how many columns fit into the container. Even when the container is quite large, the cards can remain tightly packed into smaller grid cells.

There's currently no way to declare those grid cells as "containers" directly. Instead, an extra element is needed to measure within each cell. That's why each product-detail instance has an <article> element nested directly inside. product-detail > article is the card to be styled, while product-detail itself is used only as a container to measure. That allows the cqi-based text and spacing calculations previously defined to be recalculated for the space available to each card.

Explicit container queries

Container units are powerful, but sometimes it's useful to make more dramatic changes in a component layout when the available size crosses a threshold. These are often called breakpoints—since the fix is applied at the point when a given layout begins to break. You may already be familiar with using @media to add breakpoints based on the viewport size:

main {
  display: grid;
  grid-template:
    'controls' auto
    'cart' 1fr
    'list' auto
    / minmax(min-content, 1fr);

  @media (width > 30em) {
    grid-template:
      'controls controls' auto
      'list cart' 1fr
      / 2fr 1fr
    ;
  }
}

Here, shopping-cart appears above the main product-list on small screens—but at a certain point, there's more horizontal space, and a sidebar makes more sense. Since that shift depends on the overall viewport, a media query is used to handle the change. However, the product-list and product-detail components might appear in different contexts, somewhat independent of the viewport size. When the viewport grows wider than the sidebar breakpoint, the shopping-cart component suddenly becomes smaller, providing less space for products inside. This is where container queries become necessary. The syntax is nearly identical to a media query, but uses @container rather than @media at the start of the rule:

article {
  display: grid;
  grid-template:
    'image' auto 'title' auto
    'summary' auto
    'button' auto / auto
  ;

  @container (inline-size > 40ch) {
    --image-ratio: 1;
    grid-template:
      'image title' auto
      'image summary' 1fr
      'image button' auto
      / minmax(50px, min(20%, 500px)) 1fr
    ;
  }

  @container (inline-size > 50ch) {
    grid-template:
      'image title title' auto
      'image summary button' 1fr
      / minmax(50px, min(20%, 500px)) 1fr fit-content(20%);
  }
}

The initial goal of this post is that components should be able to respond to any context. To make that work, each component defines its own internal behavior, without explicit knowledge of the surrounding components. Container queries help us accomplish that. The product-list and product-detail components don't need to be aware of why they have more or less space in a given context, they only need to know how much space is currently available.

Transition grid templates and visibility

With a layout that's changing often based on container queries, how do we smooth out all of the transitions? When using grids for layout, it's possible to animate the size of a column or row, as well as the gap between columns and rows. In this case, the cart area and gap are expanded from 0 width when the cart is opened. In order to animate grid templates like this, two things are required:

  1. The initial and end states must have the same number of tracks (columns or rows).
  2. The animated tracks must use comparable units.

While it would be possible to change from a one-column grid to a two-column grid when the sidebar is hidden, the empty sidebar column is instead resized to 0, and the column-gap is also set to 0. When the cart is open in the sidebar, the 0-width column transitions to calc(15em + 1cqi). Since the calculation results in a normal length value, the transition can be animated from length 0. The gap also animates from one length to another—from 0 to var(--gap)—which was defined earlier:

main {
  transition: grid-template 250ms, gap 250ms;
}

Non-Baseline animation enhancements

The shopping-cart and product-detail components are also animated when hidden or shown, and this is done using two recent features that are not yet Baseline, but work as progressive enhancements for browsers that do have support:

  1. Applying interpolate-size: allow-keywords allows transitioning element dimensions from 0 to auto. This is used for transitioning the products to and from a block-size of 0 when they are added or removed from the cart. Since the interpolate-size property inherits, that only needs to be defined once on the <html> element, and it is available to every other element on the page. This is only supported in Chrome-based browsers (Chrome, Edge, and others), but can be used as a progressive enhancement. The fallback works as expected, just without the animated transition.

    Browser Support

    • Chrome: 129.
    • Edge: 129.
    • Firefox: not supported.
    • Safari: not supported.

    Source

  2. "Discrete" properties like display can now be transitioned as well, even though there are no intermediate values between grid and none. Instead the transition is applied at the start or end of the duration provided. To achieve that, allow-discrete is added to the transition-behavior property. While transition-behavior is otherwise Baseline, animating the display property is not yet supported in Firefox. But again, this works well as a progressive enhancement.

    Browser Support

    • Chrome: 117.
    • Edge: 117.
    • Firefox: not supported.
    • Safari: 18.

Querying media versus containers

There are many situations where container queries and units can be used to replace media queries and viewport units, and that's great when it helps you express the intent of a design more clearly. But one is not meant to replace the other, and there's not a better query or unit that will work in every situation. CSS works best when the relationships established in code match the goals and purpose of the design. When you want text and spacing that is relative to immediate context, container queries provide that functionality, but when you want consistent sizing relative to the overall viewport, media queries and viewport units are still an excellent option. Most websites will likely involve a mix of both.