Container Queries vs Media Queries: Page Decisions and Component Decisions
Learn when to reach for a media query and when a container query fits better, with card, dashboard and unit examples that make reusable components adapt.
This article is published in English.
Sources: Container Queries vs Media Queries: How Modern CSS Changes Responsive Design
Most responsive CSS still hinges on one input: the width of the browser window. That works well for the overall page, but it falls apart for reusable components, because a card rendered in a sidebar and the same card rendered in a wide main column see exactly the same viewport while having very different amounts of room. This guide shows how container queries close that gap, where media queries remain the better tool, and how to combine both with fluid units into a responsive system you can reason about.
A classic viewport-driven rule looks like this: once the window drops below a breakpoint, the card stacks its content vertically.
@media (max-width: 768px) {
.card {
flex-direction: column;
}
}
Nothing about this is broken. The weakness is conceptual: the rule learns how wide the window is, never how wide the card is. In a small demo those numbers track each other; in an application full of grids, sidebars and panels they drift apart, and that gap is where layout bugs appear. Container queries let a component ask "how much space do I actually have?" instead of "how wide is the screen?".
Why a viewport breakpoint misleads a reusable card
Take a straightforward product card: an image next to a block of text with a title, a short description and a call to action. The markup is split across two snippets here, but it is one element.
<article class="product-card">
<img src="product.jpg" alt="Product">
<div class="product-info">
<h2>Wireless Headphones</h2>
<p>Noise cancellation with 30-hour battery life.</p>
<button>Buy now</button>
</div>
</article>
The first styling pass usually puts image and text side by side with flexbox, then adds a viewport breakpoint to stack them on small screens.
.product-card {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.product-card {
flex-direction: column;
}
}
That seems fine until the card is reused. On a wide desktop page it has plenty of horizontal room, and the side-by-side layout looks right.
Desktop page
┌───────────────────────────────────────────┐
│ │
│ Product Card │
│ ┌──────────┐ ┌─────────────────────┐ │
│ │ Image │ │ Product information │ │
│ └──────────┘ └─────────────────────┘ │
│ │
└───────────────────────────────────────────┘
Drop the very same card into a narrow sidebar, and it needs the stacked layout even though the window is still desktop-sized.
┌───────────────┐
│ │
│ Sidebar │
│ │
│ ┌───────────┐ │
│ │ Image │ │
│ ├───────────┤ │
│ │ Info │ │
│ └───────────┘ │
│ │
└───────────────┘
Place a few of them in a dashboard row, and each card gets a different share of the width depending on its column.
┌───────────────────────────────────────────┐
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Card │ │ Card │ │
│ └─────────────┘ └──────────────────────┘ │
└───────────────────────────────────────────┘
Three placements, three different widths, one unchanged viewport. A rule keyed to the window cannot tell them apart, so the sidebar card stays cramped.
What media queries are actually good at
A media query evaluates properties of the environment the page is displayed in: viewport dimensions, orientation, color scheme, pointer type, reduced-motion preferences and so on. A width query is the familiar case.
@media (max-width: 768px) {
...
}
The flow of information is simple: the browser measures the viewport, the query matches or not, and the layout reacts.
Viewport
↓
Media Query
↓
Layout changes
That makes media queries the right instrument for decisions that genuinely belong to the page as a whole. A typical example is a two-column shell with a fixed-width sidebar:
.page-layout {
display: grid;
grid-template-columns: 1fr 300px;
}
Below a certain window width, the shell collapses to a single column:
@media (max-width: 900px) {
.page-layout {
grid-template-columns: 1fr;
}
}
That is a sound use of a media query: whether the page has room for a sidebar really is a viewport question. Trouble starts when the same mechanism decides how an individual component arranges its internals.
How container queries work
A container query lets an element respond to the size of an ancestor that you have explicitly marked as a query container. Setting it up takes two steps. First, declare the container:
.card-container {
container-type: inline-size;
}
The value inline-size tells the browser that descendants may query the container's inline dimension, which is its width in horizontal writing modes. Second, write a rule against that container:
@container (max-width: 500px) {
.product-card {
flex-direction: column;
}
}
The information now flows from the container rather than the window:
Container
↓
Container Query
↓
Component layout
The card no longer cares whether the visitor uses a phone or a large monitor; it reacts to the space its parent gives it.
One detail that trips people up
A container query styles the descendants of the container, never the container itself. That is why the examples always pair a wrapper (.card-container, .card-wrapper) with an inner element (.product-card). If you put container-type on .product-card and then query it to change .product-card's own flex-direction, nothing happens. Keep the wrapper and the component as separate elements.
A second detail: inline-size containment means the container's width can no longer be derived from its content. A container that is a block-level child of a normal flow layout is fine, because its width comes from its parent. A container that relies on shrink-to-fit sizing, such as an inline-block or an absolutely positioned element without an explicit width, can collapse to zero width once containment is applied.
Viewport width versus component width
The contrast becomes concrete in a dashboard with a wide main column and a narrower side column:
.dashboard {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 1rem;
}
Assume a 1440px window. A viewport-based rule for the card would look like this:
@media (max-width: 768px) {
.card {
flex-direction: column;
}
}
On that window, the rule never matches. Yet a card sitting in the narrower column may be only about 350px wide. The media query sees one number:
Viewport = 1440px
The card lives with another:
Component width = 350px
Only the second number describes the card's real conditions, and only a container query can respond to it.
A minimal container query setup
The implementation is short. Mark the wrapper as a container:
.card-wrapper {
container-type: inline-size;
}
Give the card a two-column grid when its container is at least 600px wide:
@container (min-width: 600px) {
.product-card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
Fall back to a simple block layout below that:
@container (max-width: 599px) {
.product-card {
display: block;
}
}
The markup only needs the wrapper element around the card:
<div class="card-wrapper">
<article class="product-card">
...
</article>
</div>
The card's layout now derives from its container, so moving the wrapper into another column produces the right layout automatically. Note that the two rules use min-width: 600px and max-width: 599px; with fractional widths a container can land between those values. The newer range syntax, @container (width >= 600px) and @container (width < 600px), avoids that gap and is worth preferring in new code.
Why this fits component-based architecture
Container queries matter more than a typical new CSS feature because modern frontends are assembled from reusable parts, whether those are framework components in React, Vue or Angular, framework-free Web Components, or the building blocks of a shared design system. None of them should have to know which page region they end up in. Consider a user-card element. It might render in the main content area:
<main>
<user-card></user-card>
</main>
It might render in an aside:
<aside>
<user-card></user-card>
</aside>
Or it might be repeated inside a dashboard grid:
<div class="dashboard-grid">
<user-card></user-card>
<user-card></user-card>
<user-card></user-card>
</div>
Hard-coding a viewport breakpoint into that component bakes in an assumption it has no way to verify:
@media (max-width: 768px) {
...
}
The number 768 says nothing about the component's real context. A container query offers a better contract: given this much room, use this layout. If your components are framework-agnostic building blocks, the discussion of where the framework boundary belongs in a reusable UI stack is a useful companion to this idea.
Letting a profile card adapt to its surroundings
Here is a compact profile card that places an avatar next to a name and role:
.profile-card {
display: flex;
gap: 1rem;
align-items: center;
}
Its wrapper becomes a query container:
.profile-container {
container-type: inline-size;
}
When that container is 400px wide or less, the card stacks and centers its content:
@container (max-width: 400px) {
.profile-card {
flex-direction: column;
text-align: center;
}
}
In a roomy container, the avatar sits beside the text:
┌──────────────────────────────────────┐
│ [Avatar] John Smith │
│ Senior Developer │
└──────────────────────────────────────┘
In a tight one, the same component rearranges itself vertically:
┌──────────────────┐
│ │
│ [Avatar] │
│ │
│ John Smith │
│ Senior Developer │
│ │
└──────────────────┘
No resize observer, no second "compact" variant, no class toggled by the parent page.
Media queries still have a job
Container queries do not replace media queries; they answer a different question. A quick way to sort common decisions:
- Collapsing the main navigation: media query.
- Removing the page-level sidebar: media query.
- Switching a card between horizontal and vertical layout: container query.
- Adapting a reusable component to its slot: container query.
- Adjusting typography across the whole site: media query.
- Letting a dashboard widget respond to its column width: container query.
- Switching the entire application shell to a different layout: media query.
- Changing a component's behavior inside a narrow parent: container query.
A mental model: global decisions and component decisions
The clearest way to split the work is to separate what the page decides from what each component decides.
Media queries ask about the viewport
The question behind a media query is:
"What is the viewport doing?"
They suit:
- Page structure
- Navigation
- Global spacing
- Global typography
- Major layout changes
Container queries ask about the component's surroundings
The question behind a container query is:
"What is my component's environment doing?"
They suit:
- Cards
- Widgets
- Panels
- Reusable UI components
- Dashboard modules
- Design-system components
Sizing with container query units
Container queries are not only conditional blocks. CSS also defines length units relative to the nearest query container, so values can scale continuously instead of jumping at a breakpoint:
.title {
font-size: 5cqi;
}
Here cqi means one percent of the container's inline size, so 5cqi is five percent of its width. The full set of units:
cqw → container width
cqh → container height
cqi → container inline size
cqb → container block size
cqmin → smaller container dimension
cqmax → larger container dimension
A common pattern wraps a container unit in clamp() so the value scales with the container but never leaves a sensible range:
.card-title {
font-size: clamp(1rem, 4cqi, 2rem);
}
The title now scales with the component's space rather than the window, which viewport units such as vw cannot express. Two caveats. If an element has no ancestor query container, these units fall back to small viewport units, so they will still produce a value, just not the one you intended. And keeping a rem floor and ceiling inside clamp(), as above, matters for accessibility: a font size driven purely by container width does not respond to the user's text-size settings.
Naming containers in larger applications
For a small page, an anonymous container is enough:
.container {
container-type: inline-size;
}
Once containers nest, a bare @container rule matches the nearest ancestor container, which may not be the one you meant. The container shorthand lets you give a container a name along with its type:
.dashboard {
container: dashboard / inline-size;
}
Rules can then target that container explicitly:
@container dashboard (min-width: 700px) {
.widget {
grid-template-columns: 1fr 1fr;
}
}
Compare an unnamed condition:
@container (min-width: 700px)
with a named one:
@container dashboard (min-width: 700px)
The named version states its intent and keeps working even if someone later adds another container between the dashboard and the widget.
Choose container boundaries deliberately
Container queries are powerful, but that is not a reason to add this declaration to every element:
container-type: inline-size;
Each container applies layout, style and inline-size containment, which changes how that element can be sized, so containers should mark meaningful component boundaries rather than being sprinkled everywhere. Consider a dashboard with widgets inside it:
<section class="dashboard">
<article class="widget">
<div class="widget-content">
...
</div>
</article>
</section>
A sensible choice is to make the dashboard the named container:
.dashboard {
container: dashboard / inline-size;
}
and let each widget respond to it. The rule is not "use container queries everywhere" but "use them where behavior depends on available space".
Replacing breakpoint stacks with content-driven breakpoints
Plenty of stylesheets contain a ladder of viewport breakpoints like this:
@media (max-width: 1200px) {
...
}
@media (max-width: 1024px) {
...
}@media (max-width: 900px) {
...
}@media (max-width: 768px) {
...
}@media (max-width: 640px) {
...
}@media (max-width: 480px) {
...
}
Sometimes that ladder is justified. Often it signals that components were designed around device widths rather than their own content. Container queries push you toward a different question. Instead of "which breakpoint does the team usually use?", ask "at what width does this component stop looking right?" The answer gives you a content-driven breakpoint:
@container (max-width: 420px) {
.card {
flex-direction: column;
}
}
Nothing about 420px corresponds to a particular phone; it is where this card runs out of room. Breakpoints chosen this way survive layout changes and new devices.
Designing components that do not depend on placement
Container queries also nudge teams toward genuinely independent components, whose behavior is predictable wherever they are rendered. Picture a design-system button group. It might live inside a form on a page:
Page
└── Form
└── Button Group
inside a card on a dashboard:
Dashboard
└── Card
└── Button Group
or inside a card in a sidebar:
Sidebar
└── Card
└── Button Group
The viewport is identical in all three cases; the space is not. With a container query, the group adapts without knowing anything about the structure above it.
A reusable responsive card pattern
Here is a complete pattern you can adopt directly. The markup wraps the card in a container element; again, the HTML is shown in two consecutive pieces.
<div class="card-container">
<article class="card">
<img src="image.jpg" alt="">
<div class="card-content">
<h2>Performance Dashboard</h2>
<p>
Monitor application performance and infrastructure metrics.
</p>
<a href="#">View dashboard</a>
</div>
</article>
</div>
The wrapper is declared as a container:
.card-container {
container-type: inline-size;
}
The card starts as a single-column grid with a 16:9 image. Once its container reaches 600px, it switches to a fixed image column beside the content and makes the image square. The rules are written without line breaks between them, which is valid CSS but worth reformatting when you paste it into a codebase.
.card {
display: grid;
gap: 1rem;
}.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}@container (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr;
align-items: center;
} .card img {
aspect-ratio: 1;
}
}
Notice what this stylesheet does not contain:
@media (max-width: 768px)
The card never consults the viewport. Everything it decides is based on its container.
Pairing container queries with an auto-fitting grid
Container queries combine especially well with a grid that decides its own column count:
.dashboard {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
With repeat(auto-fit, minmax(280px, 1fr)), the browser fits as many columns of at least 280px as the row allows and stretches them to fill the leftover space. Each widget then gets its own container:
.widget-container {
container-type: inline-size;
}
and switches to a two-column internal layout when its slot is wide enough:
@container (min-width: 500px) {
.widget {
grid-template-columns: 1fr 1fr;
}
}
The dashboard adapts across a wide range of widths without viewport breakpoints: the grid decides how much space each widget gets, and each widget decides how to use it.
Responsive layout is also a usability concern
Fitting on screen is not the same as being easy to use. A row of buttons that technically fits in a narrow card can still be cramped and error-prone to tap. A container query can stack the actions and stretch them to full width when the card is narrow (the rule is split across two snippets):
@container (max-width: 350px) {
.actions {
flex-direction: column;
}
.actions button {
width: 100%;
}
}
Full-width stacked buttons give larger touch targets and reduce accidental taps. Type can follow the same logic, scaling within bounds as the component's width changes:
.card-title {
font-size: clamp(1rem, 4cqi, 1.75rem);
}
The specific values matter less than the principle behind them: responsive behavior should reflect the conditions a component is actually used in, not a guess derived from the window size.
Where media queries remain the right choice
Container queries are not automatically the better option. Global navigation is a clear case for a media query, because hiding the full menu and showing a menu button is a decision about the page shell (this rule is also split in two):
@media (max-width: 900px) {
.navigation {
display: none;
}
.menu-button {
display: block;
}
}
User preferences are another category entirely. Reducing animation for people who have asked their operating system for less motion has nothing to do with any container:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms;
animation-iteration-count: 1;
}
}
That is an environment preference, which only a media query can read. The aim is not to eliminate media queries, only to stop using them for component decisions.
A three-layer approach to responsive CSS
For a component-based application, it helps to think about responsive styling in three layers.
Layer 1: global responsiveness with media queries
Media queries own the concerns that span the whole page:
Page layout
Navigation
Global typography
Major structural changes
Application shell
Layer 2: component responsiveness with container queries
Container queries own the internal layout of self-contained pieces:
Cards
Widgets
Panels
Reusable components
Design-system components
Dashboard modules
Layer 3: fluid behavior with functions and units
Where no hard breakpoint is needed, let values scale continuously with modern functions and units:
clamp()
min()
max()
minmax()
cqi
cqw
A heading, for example, can grow with its container between two fixed limits:
.heading {
font-size: clamp(1.5rem, 3cqi, 3rem);
}
Together, these layers form a system rather than a pile of breakpoint patches.
The real change is who owns the decision
It is tempting to summarize container queries as "media queries for elements". That is technically accurate and misses the point. What changes is the responsibility model. In traditional responsive CSS, the page effectively dictates how each component behaves:
Viewport
↓
Media Query
↓
Component
With container queries, each component takes its cue from its own container:
Component Container
↓
Container Query
↓
Component
Components that carry their own responsive rules are easier to reuse, move, test in isolation and maintain. That shift in ownership is the lasting benefit.
One question to settle every case
When you are unsure which tool to use, ask whether the layout decision is about the page or about the component. If it concerns the page, use a media query. If it concerns the space a component has been given, use a container query. Applied to the recurring cases:
Should the sidebar disappear?
↓
Media Query
Should this card switch from horizontal
to vertical?
↓
Container Query
Should the navigation collapse?
↓
Media Query
Should this reusable widget change
its internal layout?
↓
Container Query
Key takeaways
- Media queries describe the viewport and user environment, so they fit page structure, navigation, global typography and preferences such as reduced motion.
- Container queries describe the space a component receives, so they fit cards, widgets, panels and design-system components.
- A container query styles descendants of the container; keep the container and the component as separate elements, and watch for containers that need content-based width.
- Content-driven breakpoints, set where a component actually stops working, outlive device-driven ones.
- Container units such as
cqiandcqw, wrapped inclamp()withrembounds, give fluid sizing without sacrificing user text-size settings. - Name containers once they nest, and create them only at meaningful component boundaries.
- The most robust setup layers media queries, container queries and fluid CSS rather than choosing one of them.