Mert Özen Further With Every Line
Frontend CSS3: Flexbox, Grid and Modern CSS — #09

Container Queries: Size Queries, Style Queries and Component-Based Responsive Design

Mert Özen Nov 14, 2026 12 min 2 views
Container Queries: Size Queries, Style Queries and Component-Based Responsive Design

A container query lets a component change its layout based on the width of the box it is placed in rather than the viewport; but container-type turns subgrid off, so the two cannot be used on the same element. We show the Bookstore card horizontal in a wide band and vertical in a narrow column, and measure cq units, the name-only and style queries that became Baseline in May 2026, and making an Angular host a container, in Chromium and WebKit.

Version note: This post follows the CSS Conditional Rules Level 5 and CSS Containment Level 3 specifications and the state of Baseline in November 2026; the examples were run in Chrome 158 and WebKit 27.2 via Playwright 1.65, with Angular 22.2 and TypeScript 6.0, and Firefox support is taken from web-features 3.46 data.

Guess First

In the previous Grid post we moved the catalog cards of Bookstore, the online bookstore used as the shared example across this site's series, to subgrid: every card borrows four rows from the list, and titles, authors and prices line up. Now we want the card to change its layout based on its own width; making the card a container seems to be all it takes. The lab page has a shelf of three cards, and we add a single line to the middle one:

.card.measured { container-type: inline-size; }

This line does not change the card's width, height or content; it only says "let this element's width be queried". Does the author row of the middle card stay level with the author rows of the other two cards?

In this post we take responsive design down from the page to the component. Media queries ask about the viewport; container queries ask about the box a component is placed in. We will measure size queries, cq units, the name-only and style queries that became Baseline in 2026, making an Angular component's host a container, and the price of container queries.

Why Media Queries Are Not Enough

I added two new sections to Bookstore's catalog page: a dark "Book of the week" band above the results and "Recently viewed" under the filter column on the left. Both contain the same .book-card:

<section class="featured" aria-labelledby="featured-title">
    <h2 id="featured-title">Book of the week</h2>
    <article class="book-card in-stock" data-format="hardcover ebook">
        <img src="img/dune.svg" alt="Cover of Dune" width="120" height="180">
        <h3 class="book-title">Dune</h3>
        <span class="badge">Bestseller</span>
        <p class="book-author">Frank Herbert</p>
        <p class="price">349.00 TRY</p>
    </article>
</section>

On a 1280px screen one of these cards lands in a 992px wide band, the other in a 192px column. The viewport is 1280px for both. If you write a rule with @media (width >= 64rem), both cards make the same decision: either both go horizontal and the card in the left column gets squeezed into 192px, or both stay vertical and the wide band is left empty. A media query knows the width of the page, not where the card was placed. The more you reuse components, the bigger this problem gets: the same card sits at different widths in the catalog, the cart, the "similar books" on the book detail page and the admin panel.

Size Queries and cq Units

A container query has two steps. First you make an element a container, then you style the elements inside it based on its size. I made the two slots holding the cards containers:

/* slots for a single card: the card inside adapts to the slot's width, not to the viewport */
.featured,
.recently-viewed {
    container: book-card / inline-size;
    margin-block-end: var(--space);
}

/* a dark band: components inside read --surface with a style query */
.featured {
    --surface: dark;
    padding: var(--space);
    border-radius: var(--radius);
    background-color: #1f2937;
    color: #f9fafb;
}

/* only a name, no size containment: cards can ask "am I in the sidebar?" */
.filters {
    container-name: sidebar;
}

container: book-card / inline-size is shorthand for container-name: book-card and container-type: inline-size. inline-size says that only the width can be queried; for that the browser applies inline-size containment to the element, so it takes its width from outside, not from its content. On the card side the rules are these:

/* wide slot: cover on the left, text on the right */
@container book-card (width >= 32rem) {
    .book-card {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-rows: auto auto 1fr;
        grid-template-areas:
            "cover title"
            "cover author"
            "cover price";
        column-gap: var(--space);
    }

    /* 1cqi = 1% of the slot's inline size */
    .book-card .book-title {
        font-size: clamp(1.25rem, 1rem + 1.5cqi, 2rem);
    }
}

/* name-only query: any card in the sidebar gets a smaller cover */
@container sidebar {
    .book-card > img {
        inline-size: 4rem;
        block-size: auto;
    }
}

/* style query: the slot says it is dark, the card follows */
@container style(--surface: dark) {
    .book-card {
        border-color: #4b5563;
        background-color: #111827;
        color: #f9fafb;
    }

    .book-card .book-author {
        color: #d1d5db;
    }
}

@container book-card (width >= 32rem) looks at the width of the nearest ancestor named book-card. These rules can style .book-card itself, because the container is not the card but its slot. If the slot is wider than 32rem, the card switches to two columns: the cover on the left across three rows, title, author and price on the right. The cqi unit is one percent of the container's inline size; the component-level counterpart of vw. The measurement:

node scripts/09-container.mjs
--- chromium
1280px featured        w=992 cover 120px, title at x=153 y=17 size 30.88px, columns "120px 822px", background rgb(17, 24, 39)
1280px recently viewed w=192 cover 64px, title at x=17 y=117 size 18.72px, columns "158px", background rgb(255, 255, 255)
1280px list            w=244 cover 120px, title at x=17 y=201 size 24px, columns "210px", background rgb(255, 255, 255)
360px featured        w=296 cover 120px, title at x=17 y=201 size 18.72px, columns "262px", background rgb(17, 24, 39)
360px recently viewed w=328 cover 64px, title at x=17 y=117 size 18.72px, columns "294px", background rgb(255, 255, 255)
360px list            w=328 cover 120px, title at x=17 y=201 size 24px, columns "294px", background rgb(255, 255, 255)

At 1280px three different cards show up on the same page. The card in the band is 992px wide and horizontal: columns 120px 822px, the title to the right of the cover (x=153) and at the top (y=17). The title size is 30.88px; since the band's content width is 992px, 1cqi = 9.92px and 1rem + 1.5cqi = 16 + 14.88. The card in the left column is 192px and vertical; the card in the list is vertical too, and uses subgrid. At 360px all three cards are vertical, because the band is now 296px. WebKit gave the same values; it only prints font sizes with longer decimals such as 30.879999px.

The cards in the list are not affected by these rules at all, because none of their ancestors is a container named book-card. When a query finds no matching container, it counts as false, and the card stays in its default, vertical layout. I did that on purpose; the reason is in the next section.

Subgrid and a Container Query Do Not Work on the Same Element

The lab page for the guess looks like this:

/* 1) subgrid cards; the second one is also a size container */
.shelf { display: grid; grid-template-columns: repeat(3, 180px); gap: 16px; }
.card { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 4px; }
.card.measured { container-type: inline-size; }

/* 2) a container cannot style itself with its own query */
.panel { container-type: inline-size; inline-size: 400px; }
@container (width >= 300px) {
    .panel { outline: 2px solid; }
    .panel p { font-weight: 700; }
}

/* 3) size containment in both axes: the height no longer comes from the content */
.both { container-type: size; }
--- chromium
Middlemarch  author y=69   rows "subgrid [] [] [] []"
Dune         author y=40   rows "none"
Emma         author y=69   rows "subgrid [] [] [] []"
panel: outline none, text weight 700
container-type: size, height 0px

The author row of the Dune card in the middle is at 40px, the other two cards' at 69px. Adding container-type: inline-size turned subgrid off, and Chromium reports the used value of grid-template-rows as none. WebKit's output is interesting:

--- webkit
Middlemarch  author y=69   rows "subgrid [] [] [] []"
Dune         author y=40   rows "subgrid"
Emma         author y=69   rows "subgrid [] [] [] []"

WebKit still says subgrid in the computed value, but the layout is the same: Dune's author is at 40px. This is not a browser bug; the specification requires it. According to CSS Conditional Rules Level 5, container-type: inline-size applies style and inline-size containment to the element and establishes an independent formatting context. CSS Grid Level 2 in turn says that a grid forced to establish an independent formatting context (layout containment and absolute positioning are given as examples) cannot be a subgrid. In short, an element either takes part in its parent's rows or has its size queried independently of the outside world; it cannot do both.

That is why I split the work in Bookstore. In the catalog list alignment matters, and the cards are already between 14rem and 28rem, so the list never needs a horizontal layout: subgrid lives there. In single-card slots (the band, the left column) there are no neighbours to line up with: container queries live there. Seeing a conflict like this at design time is much cheaper than applying both techniques to the same card and hunting for the reason later.

A Container Cannot Query Itself, and Size Containment Can Collapse the Height

The last two lines of the output show two more pitfalls. .panel is a 400px wide container, and inside @container (width >= 300px) there are rules both for the panel itself and for the paragraph inside it. The paragraph became bold (font-weight 700), but the panel's own outline is none. A container query does not ask about the element itself but about its nearest container ancestor; the panel has no container among its ancestors, so the panel's own rule never matched. If you also want to change a component's own box, either put it into a slot (like .featured in Bookstore) or add a wrapper element inside the component.

The second pitfall is container-type: size. This value applies containment on both axes: the element's height no longer comes from its content either. .both in the lab has a paragraph inside, but its height is 0px; the paragraph overflows the box. Use size only on elements whose height is already set from outside (a fixed-height panel, a grid cell). For horizontal layout changes, inline-size is almost always enough.

Name-Only and Style Queries

Two new kinds of container query became Baseline in 2026. A name-only query asks only for the name, without a condition: @container sidebar { … } means "if I am inside a container named sidebar". In Bookstore I gave the filter column only container-name: sidebar, no container-type. That matters: since name-only and style queries do not ask about size, they need no containment, so they do not break subgrid or the height calculation. Every card in the left column shows a small cover; cover 64px in the output. Name-only queries became Baseline in May 2026.

A style query asks about the value of a custom property on the container. The dark band says --surface: dark; the card darkens its own background and text colour with @container style(--surface: dark). background rgb(17, 24, 39) in the output is the result of that rule. We could have written the same thing with a .featured .book-card selector; the difference with a style query is that the card does not need to know the band's class name. It is enough for the band, a dialog or a dark footer to say --surface: dark; the card does not ask "which dark place am I in", it asks "is the background dark". Style queries became Baseline in May 2026 with Firefox 151; Chrome had supported them since 111, Safari since 18. We will cover custom properties themselves and theming in detail in the next post of this series.

Making an Angular Component's Host a Container

In Angular every component has a host element (<app-book-card>). This host is display: inline by default and is a natural slot for container queries. I set up a small bookstore-web project in the project's angular/ folder (with TypeScript 6.0, since Angular 22.2 projects require it):

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-book-card',
  template: `
    <article class="card">
      <img [src]="cover()" [alt]="'Cover of ' + bookTitle()" width="120" height="180">
      <h3>{{ bookTitle() }}</h3>
      <p class="author">{{ author() }}</p>
    </article>
  `,
  styles: `
    /* the host element is the container: every app-book-card measures its own slot */
    :host {
      display: block;
      container: book-card / inline-size;
    }

    .card {
      display: grid;
      gap: 0.25rem 1rem;
    }

    /* a container cannot query itself, so the layout lives on the inner .card */
    @container book-card (width >= 32rem) {
      .card {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-rows: auto 1fr;
      }

      img {
        grid-row: span 2;
      }
    }
  `,
})
export class BookCard {
  readonly bookTitle = input.required<string>();
  readonly author = input.required<string>();
  readonly cover = input.required<string>();
}

The :host rule makes the host a container; the layout is on the inner .card, because, as we just saw, the host cannot query itself. The application puts the same component into a 14rem column and into a wide main area. After the build, Playwright serves the output with a small route handler; no separate web server is needed:

cd angular
npm install
npx ng build
cd ..
node scripts/09-angular.mjs
--- chromium
Emma  host w=224 container-type inline-size, .card columns "224px"
Dune  host w=768 container-type inline-size, .card columns "120px 632px"
[_nghost-ng-c207890517] { display: block; container: book-card / inline-size; }
@container book-card (width >= 32rem) { .card[_ngcontent-ng-c207890517] { grid-template-columns: auto minmax(0px, 1fr); grid-template-rows: auto 1fr; } img[_ngcontent-ng-c207890517] { grid-row: span 2; } }
--- webkit
Emma  host w=224 container-type inline-size, .card columns "224px"
Dune  host w=768 container-type inline-size, .card columns "120px 632px"
[_nghost-ng-c207890517] { display: block; container-name: book-card; container-type: inline-size; }
@container book-card (width >= 32rem) { .card[_ngcontent-ng-c207890517] { grid-template-columns: auto minmax(0px, 1fr); grid-template-rows: auto 1fr; } img[_ngcontent-ng-c207890517] { grid-row-start: span 2; grid-row-end: auto; } }

Emma in the column is 224px and single-column; Dune in the main area is 768px with 120px 632px. The last two lines show the transformation done by Angular's emulated encapsulation: :host was turned into the [_nghost-ng-c207890517] attribute selector, [_ngcontent-…] was added to the selectors inside @container, and the container query itself stayed as it was. WebKit writes the same rule with the container-name and container-type longhands. We will compare Angular's style encapsulation modes in the nesting and @scope post of this series.

Before container queries this was done with JavaScript: the component watched its own width with ResizeObserver and changed a class or a signal. The Resize Observer post in the advanced part of the JavaScript series covers that approach. For layout changes, container queries are better: the right layout arrives on the first render without any JavaScript running, and layout and style are computed in the same step. ResizeObserver still makes sense for things CSS cannot do, such as loading different data or showing a different component depending on the width.

In Production

Size queries and cq units became Baseline in February 2023 and have been widely available since August 2025. Name-only queries (May 2026) and style queries (May 2026) are newly available: if your target covers the last two browser versions you can use them directly; if you also serve older browsers, treat these rules as an extra layer of presentation. A browser without support ignores the @container block and the card stays in its default layout; in Bookstore the default layout is the vertical card, so the content is readable in every case.

Every container is a containment boundary: the browser computes the container's width without looking at its content. That is good for performance, but if a box's width depends on its content (width: fit-content, a flex item's flex-basis: auto), turning it into a container can make it narrower. Always put a container into a slot whose width is set from outside: a grid cell, a block element, or, in Angular, a host with display: block.

Common Pitfalls

First, giving a subgrid card a container-type. As we measured, subgrid switches off silently; there is no warning, only the alignment breaks.

Second, thinking a container can query itself. The rule looks at the nearest container ancestor; to change a component's own box you need a slot or a wrapper inside.

Third, using container-type: size without thinking. Every element whose height comes from its content collapses to 0px.

Fourth, trying to remove media queries altogether. The page skeleton is a viewport decision; whether the filter column moves to the side should depend on the screen width. Media queries are for page layout, container queries for component layout.

A Small Experiment

★☆☆ In lab/09-pitfalls.html, replace container-type: inline-size on .card.measured with container-name: measured. Guess where Dune's author row will go, then check with node scripts/09-container.mjs.

★★☆ Add a section titled "Similar books" to book.html, below the book summary, and put two .book-card elements in it. Make the section a book-card container and verify with a script that the cards are vertical at 360px and horizontal at 1280px.

★★★ Add an input named variant to BookCard in the Angular project and write it to the host as the --variant custom property (host: { '[style.--variant]': 'variant()' }). Change the styles inside the card with @container style(--variant: compact) and measure two different variants with Playwright.

In the next post we focus on the custom properties we started using in this post: how they live in the cascade and inheritance, how they get a type with @property, the "invalid at computed-value time" trap, and adding a dark theme to Bookstore with light-dark().