
Build Responsive Components with CSS Container Queries
A component can be perfectly responsive at the page level and still break when it moves into a sidebar, dashboard cell, modal, or wide content area. Viewport media queries cannot see that local context. They only know the browser window's dimensions.
CSS container queries solve this by letting a component respond to the size of an ancestor container. This tutorial builds a reusable article card that changes its layout wherever it is placed, explains named containers and container-relative units, adds a safe fallback, and covers the mistakes that make queries appear not to work.
Container queries versus media queries
A media query answers a page-level question such as “is the viewport at least 60rem wide?” A container query answers a component-level question such as “does this card currently have at least 34rem of inline space?” Both are useful.
- Use media queries for page navigation, global spacing, input capabilities, reduced motion, and other viewport or device concerns.
- Use container queries when a reusable component should adapt to the space its parent gives it.
The result is less coupling between a component and every page that consumes it. A product card can choose its compact or expanded layout without knowing whether it lives in search results, recommendations, or a checkout panel.
Create the component markup
Start with semantic markup that is useful before advanced CSS runs:
<section class="card-region">
<article class="article-card">
<img
class="article-card__image"
src="/images/container-queries.webp"
alt="Abstract responsive layout blocks"
width="800"
height="500"
>
<div class="article-card__body">
<p class="article-card__eyebrow">CSS Architecture</p>
<h2><a href="/articles/container-queries">Responsive components without viewport guesses</a></h2>
<p class="article-card__summary">
Let the component respond to its own available space.
</p>
<p class="article-card__meta">7 minute read</p>
</div>
</article>
</section>
The wrapper will become the query container. The card inside it is the element that changes. A size query cannot style the container itself based on its own measured size; it styles descendants of that container.
Define the compact layout first
.card-region {
container-type: inline-size;
}
.article-card {
display: grid;
gap: 1rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, CanvasText 14%, transparent);
border-radius: 1rem;
background: Canvas;
color: CanvasText;
}
.article-card__image {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
}
.article-card__body {
display: grid;
gap: .65rem;
padding: 0 1rem 1rem;
}
.article-card h2,
.article-card p {
margin: 0;
}
.article-card h2 {
font-size: clamp(1.15rem, 1rem + 1.2cqi, 1.6rem);
line-height: 1.2;
}
container-type: inline-size creates a size containment context for the container's inline dimension. Inline size maps to width in common horizontal writing modes, while remaining friendlier to other writing modes than hard-coding a physical direction.
The compact one-column layout is the default. That gives older or unusual browsers a usable result and avoids making the wide state the baseline.
Add a named container and wide layout
Naming the context prevents a deeply nested component from accidentally querying a nearer, unrelated container:
.card-region {
container: article-card-region / inline-size;
}
@container article-card-region (width >= 34rem) {
.article-card {
grid-template-columns: minmax(12rem, 2fr) 3fr;
align-items: stretch;
}
.article-card__image {
height: 100%;
aspect-ratio: auto;
}
.article-card__body {
align-content: center;
padding: 1.5rem;
}
}
The container shorthand assigns both the name and type. When .card-region reaches 34rem, only cards inside that named context switch to the two-column layout.
Reuse the card in different page regions
.page-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem);
gap: 2rem;
}
.main-feed {
display: grid;
gap: 1.5rem;
}
.sidebar {
display: grid;
align-content: start;
gap: 1rem;
}
Place the same card-region and card markup in both columns. The main feed may cross 34rem and show the horizontal version while the sidebar remains stacked. No sidebar modifier class and no JavaScript resize observer are required.
Use container-relative units without runaway text
Container query units are relative to the query container. For example, 1cqi is one percent of its inline size. They are helpful for fluid internal spacing and type, but always give them boundaries:
.article-card__body {
padding: clamp(1rem, 2.5cqi, 1.75rem);
}
.article-card h2 {
font-size: clamp(1.2rem, 1rem + 1.5cqi, 1.75rem);
}
Without an eligible query container, container units fall back according to viewport-relative behavior defined for their axis. A clamp() prevents the fallback or an unusually large container from producing unreadable text.
Combine container and media responsibilities
Do not replace every media query. Accessibility preferences remain media features:
.article-card {
transition: transform 180ms ease;
}
@media (prefers-reduced-motion: reduce) {
.article-card {
transition: none;
}
}
@container article-card-region (width >= 48rem) {
.article-card__summary {
max-width: 60ch;
}
}
The container handles local layout. The media query respects a user preference. Keeping those responsibilities separate makes the CSS easier to reason about.
Add a progressive fallback
The base card already works without container queries. If you need a desktop enhancement for older engines, guard the modern rules and supply a conservative media-query fallback:
@supports (container-type: inline-size) {
.card-region {
container: article-card-region / inline-size;
}
@container article-card-region (width >= 34rem) {
.article-card {
grid-template-columns: minmax(12rem, 2fr) 3fr;
}
}
}
@supports not (container-type: inline-size) {
@media (width >= 64rem) {
.main-feed .article-card {
grid-template-columns: 2fr 3fr;
}
}
}
Check the browser matrix required by your product before deciding how much fallback code to keep. Avoid shipping two complex layout systems indefinitely when your supported environments no longer need the older path.
Troubleshooting container queries
The query never matches
Inspect the ancestor in DevTools. It must have an eligible container-type, and the queried element must be its descendant. Confirm that the container actually reaches the threshold after padding, grid tracks, and neighboring columns are calculated.
The wrong container controls the component
An unnamed query uses the nearest eligible ancestor. Give important contexts a container-name and reference it explicitly.
The container collapses or layout changes unexpectedly
Containment changes how sizes are calculated. Prefer inline-size for width-driven components. Avoid size unless you intentionally need both axes and can provide stable dimensions.
The component overflows in a grid
Grid children often need min-width: 0, or their parent track should use minmax(0, 1fr). Long URLs and unbroken strings also need an overflow strategy such as overflow-wrap: anywhere.
Production checklist
- Start with a usable compact layout before adding queries.
- Put containment on an ancestor, not the element being queried.
- Use named containers in nested design systems.
- Choose thresholds from content behavior rather than device labels.
- Bound container-relative sizes with
clamp(). - Keep accessibility preferences in media queries.
- Test narrow sidebars, wide panels, zoom, long text, and translated content.
- Verify fallbacks against the browsers your product supports.