A Practical Guide to CSS Container Queries — Responsive Design Based on Components, Not the Viewport
I've been there before. I'd carefully write all my media queries, then move the same card component into a sidebar — and the layout would completely fall apart. I thought, "I'll just add another class," but one day I found more than ten similar media queries piling up, and I realized something was fundamentally wrong. This article covers everything in one place: the three CSS Container Query types (Size Queries, Style Queries, and Scroll-State Queries), core syntax like @container and container-type, patterns you can use in production right away, and the common pitfalls to avoid. If you've worked with CSS media queries before, you'll be right at home.
In the 2025 State of CSS survey, 41% of respondents said they'd already used Size Queries, while 36% and 43% had Style Queries and Scroll-State Queries on their learning lists, respectively. Major browser support for Size Queries has surpassed 93%, so there's no longer any reason to hesitate about using them in production. That said, Style Queries and Scroll-State Queries still have limited support — we'll cover that in detail below.
Core Concepts
How is this different from media queries?
Media queries apply styles based on the size of the browser window (the viewport). In modern layouts, however, the same component often appears simultaneously in a full-width main area and a narrow sidebar. When the viewport is 1200px wide, both the card in the sidebar and the card in the main area would receive the same "wide" styles — and that's exactly the problem.
Container Queries shift the question. Instead of asking "how big is the browser window?", they ask "how big is the box containing this component?" This allows a component to adapt to its context no matter where it's placed. That's the core idea behind Intrinsic Design, a concept frequently referenced in modern frontend architecture.
📖 Intrinsic Design: A design philosophy championed by Jen Simmons, where components understand their own context (container size, state, etc.) and make their own layout decisions. It became truly practical with the arrival of Container Queries.
The Three Query Types
Container Queries fall into three main categories.
| Type | What it asks | container-type value |
|---|---|---|
| Size Queries | The container's width or height | inline-size / size |
| Style Queries | CSS custom property values | No separate declaration needed (see below) |
| Scroll-State Queries | Whether a sticky element is stuck, snap state, etc. | scroll-state |
Basic Syntax: Just Two Steps
/* Step 1: Declare a container on the parent element */
.card-wrapper {
container-type: inline-size;
container-name: card; /* The name is optional, but useful for distinguishing multiple containers */
}
/* Step 2: Style child elements based on the container's size */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}Honestly, I was confused at first about where to declare container-type. The key is that you declare it on the parent of the element whose styles you want to change. Styles inside @container apply only to child elements, not to the container itself (Scroll-State Queries are an exception — explained below).
📖
inline-sizevssize:inline-sizeonly allows querying along the horizontal axis (relative to writing direction), whilesizeenables querying both horizontally and vertically.inline-sizeis sufficient for most cases; usesizeonly when you need height-based layout logic.
Style Queries: Conditional Branching with CSS Variables
Style Queries let you use CSS custom property (variable) values as conditions. Interestingly, custom-property-based Style Queries work without declaring container-type at all. Specifying container-name is enough.
/* Works with just container-name, no container-type needed */
.btn-wrapper {
container-name: btn;
}
:root {
--btn-variant: default;
}
@container btn style(--btn-variant: danger) {
.btn {
background: red;
color: white;
}
}
@container btn style(--btn-variant: success) {
.btn {
background: green;
color: white;
}
}You can trigger changes to --btn-variant by setting it as an inline style from JavaScript.
// Apply the danger variant to a specific button wrapper
document.querySelector('.btn-wrapper').style.setProperty('--btn-variant', 'danger');
// Reset it back
document.querySelector('.btn-wrapper').style.removeProperty('--btn-variant');Using CSS custom properties as conditions makes this a natural fit for design token systems and theme switching. Note, however, that as of 2026, Firefox still doesn't support this — so it's recommended to apply it using Progressive Enhancement.
Scroll-State Queries: Scroll Reactions Without JavaScript
.sticky-nav {
container-type: scroll-state;
position: sticky;
top: 0;
}
@container scroll-state(stuck: top) {
.sticky-nav {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
}
}This pattern adds a shadow only when the sticky element is actually pinned to the top of the screen. Something that used to require an IntersectionObserver implementation can now be handled in a few lines of CSS.
💡 Exception — Styles Can Apply to the Container Itself: Unlike regular Size Queries, Scroll-State Queries allow styles inside
@container scroll-state(...)to target the container element itself. This exception exists because Scroll-State reflects the container's own scroll state.
Support began in Chrome 133 (early 2025), and Chrome 144 added the ability to query the scrolled state (most recent scroll direction).
Real-World Application
Example 1: A Card Component Used in Both a Sidebar and a Main Area
This is one of the most common situations in production work. When the same card needs to exist in a narrow sidebar (vertical layout) and a wide main area (horizontal layout) at the same time, without Container Queries you'd have to create separate classes or write duplicated, complex media queries. When I introduced this pattern, I was able to eliminate nearly half of the card-related media queries I had.
/* Naming the container prevents confusion when nesting */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Default: narrow space → vertical layout */
.card {
display: block;
}
.card__image {
width: 100%;
aspect-ratio: 16 / 9;
}
/* 500px and above: switch to horizontal layout */
@container card (min-width: 500px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
.card__image {
aspect-ratio: 1;
}
}| Placement | Behavior |
|---|---|
| Sidebar | Container is narrow, so vertical layout is maintained automatically |
| Main area | Switches to a horizontal grid when the container is 500px or wider |
| Breakpoint basis | Only the container width matters — the viewport is irrelevant |
Example 2: Scroll-Reactive Shadow on a Sticky Header
You can apply a shadow and blur effect only when the sticky header is actually pinned — no JavaScript IntersectionObserver required.
header {
container-type: scroll-state;
position: sticky;
top: 0;
background: white;
transition: box-shadow 0.2s ease, backdrop-filter 0.2s ease;
}
@container scroll-state(stuck: top) {
header {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.8);
}
}⚠️ Warning: Scroll-State Queries are currently only supported in Chrome, Edge, and Opera. They don't work in Safari or Firefox, so design things so that the absence of a shadow doesn't break functionality.
Example 3: Adaptive Layout for Dashboard Widgets
This pattern automatically adjusts how a chart widget is displayed based on the width of its column. It's especially useful for dashboard UIs where widgets can be resized by dragging.
.widget-container {
container-type: inline-size;
container-name: widget;
}
/* Default: show only the numeric summary */
.widget__chart {
display: none;
}
.widget__summary {
font-size: 2rem;
font-weight: bold;
}
/* Medium size: show mini chart */
@container widget (min-width: 300px) {
.widget__chart {
display: block;
height: 80px;
}
}
/* Wide size: full chart + legend */
@container widget (min-width: 600px) {
.widget__chart {
height: 240px;
}
.widget__legend {
display: flex;
gap: 1rem;
}
}Example 4: Responsive Typography + Multi-Column
This pattern switches the body text to a two-column layout when it's placed in a wide enough area. It improves readability by splitting long text into a comfortable reading width. That said, a single column is more natural for reading flow in narrow containers — forcing two columns can actually hurt the experience — so it's recommended to apply this only at sufficient widths (700px or more).
.article-body {
container-type: inline-size;
container-name: article;
}
.article-body p {
font-size: 1rem;
line-height: 1.7;
}
@container article (min-width: 700px) {
.article-body p {
font-size: 1.125rem;
column-count: 2;
column-gap: 2rem;
}
}Pros and Cons
Container Queries aren't a silver bullet. Understanding their strengths and weaknesses — and dividing responsibilities well with media queries — is what really matters.
Advantages
| Item | Details |
|---|---|
| Component autonomy | Components determine their own layout regardless of the viewport |
| High reusability | The same component works appropriately across different contexts |
| Easier maintenance | Styles are encapsulated at the component level, reducing global media query conflicts |
| Coexists with media queries | Media queries handle overall page layout; container queries handle component internals |
| Component-design friendly | Works naturally with component-based methodologies like Atomic Design |
| Size Queries support at 93%+ | Supported in all major browsers; ready for production use |
Disadvantages and Caveats
There are still areas that warrant caution. In particular, Style Queries and Scroll-State Queries have limited support in Firefox and Safari, so be thoughtful before introducing them directly into production.
| Item | Details |
|---|---|
container-type declaration required |
Must be explicitly set on the parent — if omitted, the query won't work |
Incompatible with <img> srcset/sizes |
The browser selects the image before CSS is applied, so it can't be used with responsive images |
| Style Queries not supported in Firefox | Custom-property-based Style Queries don't work in Firefox currently |
| Scroll-State has limited support | Only supported in Chrome/Edge/Opera; not in Safari or Firefox |
| Cannot target the container itself | Styles inside @container apply only to child elements (Scroll-State Queries are an exception) |
| Increased complexity when nested | With multiple levels of nesting, it can be hard to tell which container is being referenced |
Summary of mitigations:
- Preventing missing
container-type→ It's recommended to include it as a default in component wrapper styles. - Responsive images → It's better to stick with the existing
sizesattribute approach for determining image sizes. - Style Queries not supported in Firefox → Apply with Progressive Enhancement; design core functionality to work without CSS variables.
- Limited Scroll-State support → Use only as an enhancement; avoid using it for essential styles.
- Nesting complexity → Using
container-nameexplicitly keeps things clear and manageable.
📖 Progressive Enhancement: A strategy where core functionality is designed to work in all browsers, and modern features provide an enhanced experience only in browsers that support them. It's especially well-suited for features with limited support, like Style Queries and Scroll-State Queries.
The Most Common Mistakes in Production
- Declaring
container-typeon the element itself — If you declare it directly on the element whose styles you want to change,@containerstyles won't apply to that element. Always declare it on the parent wrapper. - Trying to replace all media queries with Container Queries — For global page layout transitions (e.g., desktop ↔ mobile navigation), media queries are still the right tool. Container Queries are best focused on component-level adaptation.
- Using nested containers without
container-name— Without a name, a nested container references the nearest ancestor container, which may not be the one you intended. In complex layouts, it's a good habit to always specifycontainer-nameexplicitly.
Closing Thoughts
CSS Container Queries don't replace media queries — they finally answer a question CSS has long left unanswered: responsive design at the component level. Size Queries are already supported by over 93% of browsers, making them ready for production today. Style Queries and Scroll-State Queries are steadily expanding their support, so learning them now means you'll be able to use them naturally when the time comes.
Three steps you can take right now:
- Pick an existing card or widget component in your project, add
container-type: inline-sizeto its parent wrapper, and convert the existing media queries to@containerqueries. You can visually verifycontainer-typeis applied in the Elements panel of Chrome DevTools, which makes debugging easy. - If you have a sticky header, try adding
container-type: scroll-stateand@container scroll-state(stuck: top)to implement scroll-reactive styles without any JavaScript. You can test it immediately in Chrome. - If you maintain a design system or shared components, try combining CSS custom properties with Style Queries to control button or badge variants using CSS alone. Until Firefox support arrives, you can proceed safely by defining base styles first and adding Style Queries as an enhancement.
Next article: We'll look at a design system architecture pattern for fully isolating component styles by using CSS Cascade Layers (
@layer) and@scopetogether with Container Queries.
References
- CSS container queries | MDN Web Docs
- Using container size and style queries | MDN Web Docs
- Using container scroll-state queries | MDN Web Docs
- Container Queries (Learn CSS) | web.dev
- CSS scroll-state() queries | Chrome for Developers
- Container queries in 2026: Powerful, but not a silver bullet | LogRocket
- When and how to choose between media queries and container queries | LogRocket
- Media Queries vs Container Queries | freeCodeCamp
- A Friendly Introduction to Container Queries | Josh W. Comeau
- Scrollytelling on Steroids With Scroll-State Queries | CSS-Tricks
- CSS Container Queries: Use-Cases And Migration Strategies | Smashing Magazine
- Scroll State Container Queries | nerdy.dev
- Directional CSS with scroll-state(scrolled) | una.im