Modern CSS: Beyond Flexbox and Grid - Container Queries and Parent Selectors
For years, media queries were our only tool for responsive design. We designed layouts based on the browser viewport width, which often meant writing fragile, context-dependent styles. But with the widespread support of Container Queries and the :has() parent selector, we are entering a new era of component-driven CSS.
In this weekly post, we will explore how these features work and how you can apply them to build more resilient, self-contained components.
1. Container Queries: Components That Know Their Own Size
Unlike media queries, which respond to the viewport size, container queries respond to the size of a parent container. This is a game-changer for reusable component libraries.
The Problem with Media Queries
Consider a card component that should display in a single-column layout when small, but switch to a side-by-side layout when there is enough room:
/* Viewport-based: fragile if placed inside a sidebar! */
@media (min-width: 768px) {
.card {
display: flex;
}
}
If this card is placed in a narrow sidebar on a desktop screen, it will stretch to the flex layout even though it has very little horizontal space.
The Container Query Solution
To use container queries, we first define a container context on the parent element using container-type:
.card-container {
container-type: inline-size;
container-name: card-wrapper;
}
Now, the card component can query its closest container:
.card {
display: block;
}
@container card-wrapper (min-width: 400px) {
.card {
display: flex;
gap: 1.5rem;
align-items: center;
}
}
This card is now completely context-aware. Whether it's placed in a sidebar, a grid item, or a full-width header, it will adapt beautifully to its immediate parent.
Try it Out! (Interactive Container Query Demo)
Drag the handle in the bottom-right corner of the box below to resize the parent container. Notice how the card flips from stacked to a horizontal row layout when its width exceeds 400px—regardless of the screen size!
Responsive Element
This element monitors its own boundaries to switch layout directions dynamically.
* Drag bottom-right corner to expand horizontally
2. The :has() Selector: The Long-Awaited Parent Selector
The :has() pseudo-class is perhaps the most powerful addition to CSS in a decade. It allows you to style an element based on its children or subsequent siblings.
Example: Styling a Card Based on Its Content
Imagine you want to add a special border or background to a card only if it contains a featured badge:
/* Style the .card if it contains an element with class .badge */
.card:has(.badge) {
border: 2px solid var(--primary-color, #4f46e5);
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.1);
}
Example: Smart Form Layouts
You can also use :has() to style form fields based on validity or focus states:
/* Highlight a form group if any input inside is invalid and has been touched */
.form-group:has(input:invalid:not(:placeholder-shown)) {
border-color: #ef4444;
}
.form-group:has(input:invalid:not(:placeholder-shown)) label {
color: #ef4444;
}
Try it Out! (Interactive :has() Demo)
Check the checkbox inside the card below. The parent card container itself will highlight dynamically using pure CSS—no Javascript classes toggled!
Charming Widget
By checking the box, you trigger parent changes through CSS `:has(input:checked)`.
3. Combining Container Queries and :has()
When combined, these two tools let you build highly interactive, dynamic layouts without writing a single line of JavaScript for DOM querying.
/* Change container layout based on child state */
.dashboard-widget:has(.alert-active) {
border-left: 4px solid #f59e0b;
}
@container (min-width: 600px) {
.dashboard-widget:has(.alert-active) {
display: grid;
grid-template-columns: 1fr auto;
}
}
Conclusion
Container queries and :has() are fully supported across all modern browsers. By adopting them in our project, we can:
- Reduce our dependency on JavaScript for layout calculations.
- Write highly modular CSS where components manage their own styling based on their surrounding context.
- Keep our UI component library clean, reusable, and bulletproof.