Component-Level Responsive Design With Container Queries
Container queries address a long-standing gap in responsive design: media queries only see the viewport, but components often need to adapt to their immediate parent. A card in a narrow sidebar may need a different layout than the same card in a wide content column, and media queries simply can't tell the difference.
To use container queries, designate an element as a container with the container property (shorthand for container-type and container-name). The container-type accepts width, height, inline-size, or block-size; the latter two are logical properties that respect the document's writing mode.
main, aside {
container: inline-size;
}
Once a container is defined, the @container at-rule works much like a media query. Note the condition syntax uses the Media Queries Level 4 range style (inline-size > 30em) rather than min-width: 30em. For a card component, flipping to a horizontal flex layout when the container exceeds 30rem looks like this:
@container (inline-size > 30em) {
.card {
display: flex;
}
}
The CSS Containment Level 3 specification remains a working draft, and the syntax has already shifted since earlier articles on the topic. The examples here match the current proposal at the time of writing.
Browser support is not production-ready. Chrome offers a flag-based implementation that doesn't align with the current spec, and the available polyfill doesn't support the latest syntax. Expect wider adoption but plan cautiously.
The :has() Selector
Widely known as the "parent selector," :has() lets you select an element based on its descendants. The practical range is broader than that label suggests, as Bramus Van Damme explored — you could differentiate an image in a <figure> based on whether a <figcaption> is present, or target labels preceding invalid form inputs.
Selecting <section> elements that contain an <h2>:
section:has(h2) {
background: lightgray;
}
Selecting an <img> whose parent <section> also contains an <h2>:
section:has(h2) img {
border: 5px solid lime;
}
No mainstream browser ships it yet, but Safari Technology Preview supports it for experimentation.
Conditional Logic With @when/@else
The @when/@else at-rules bring if/else-style conditionals to CSS. The name deliberately avoids @if to prevent collision with Sass. It's useful for combining media conditions and feature checks — for instance, verifying both a minimum viewport width and subgrid support. Inside these rules, the query's leading @ is omitted:
@when media(min-width: 30em) and supports(display: subgrid) {
/* Styles for viewports over 30em, where the browser also supports subgrid */
} @else {
/* Styles for browsers that do not meet the condition */
}
This proposal lives in CSS Conditional Rules Level 5 and is at a very early, discussion-phase stage. Browser adoption within 2022 is unlikely, but the feature merits attention.
Styling Form Controls With accent-color
Form inputs such as checkboxes, radio buttons, range sliders, and progress bars have historically been awkward to brand — each browser renders them differently, and developers often hide the native control and build custom ones from pseudo-elements. The accent-color property sidesteps that by letting the user agent's default rendering take a brand color.
The property is inherited, so declaring it on :root applies it globally:
:root {
accent-color: lime;
}
It can also be scoped to individual elements:
form {
accent-color: lime;
}
input[type="checkbox"] {
accent-color: hotpink;
}
Support is solid across Chrome, Edge, Firefox, and Safari Technology Preview. Unsupported browsers gracefully fall back to default colors, keeping inputs fully usable — an ideal progressive enhancement.
Modern Color Functions
The CSS Color Module Level 4 and Level 5 specifications introduce new ways to specify and adjust colors:
hwb(): Hue, Whiteness, Blackness.lab(): Lightness plus a and b values determining the hue.lch(): Lightness, Chroma, Hue.color-mix(): Blend two colors.color-contrast(): Given a list of colors, return the one with the highest contrast against a base color.color(): Specify a color in an alternate color space such asdisplay-p3.
The relative color syntax additionally lets you derive a new color by modifying a base one.
Functions like hwb(), lab(), and lch() behave like the familiar rgb() and hsl(), with an optional alpha parameter:
.my-element {
background-color: lch(80% 100 50); // opaque color
}
.my-element {
background-color: lch(80% 100 50 / 0.5); // color with 50% transparency
}
color-mix() takes a color interpolation method as its first argument, followed by the two colors to mix:
.my-element {
background-color: color-mix(in lch, blue, lime);
}
color-contrast() accepts a base color and compares the rest, outputting the highest-contrast option; supplying an optional keyword returns the first color in the list meeting that contrast ratio:
/* Output the color with the highest contrast */
.my-element {
color: white;
background-color: color-contrast(white vs, lightblue, lime, blue);
}
/* Output the first color that meets AA contrast ratio */
.my-element {
color: white;
background-color: color-contrast(white vs, lightblue, lime, blue to AA);
}
This is particularly useful for accessible color schemes — CSS can decide whether black or white text provides the better contrast on a button's background.
Safari currently leads support, shipping hwb(), lch(), lab(), and color() since version 15, with color-mix() and color-contrast() behind a flag. Firefox supports hwb() and offers flagged access to the mixing and contrast functions. Chrome stands out as not supporting any of these yet. Fallbacks are straightforward: declare a fallback color first, then the modern function; unsupported browsers ignore the second rule.
.my-element {
background-color: rgb(84.08% 0% 77.36%);
background-color: lch(50% 100 331);
}
With that pattern, the codebase is already positioned for when support arrives.
Managing the Cascade With @layer
Cascade Layers give authors explicit control over the “Cascading” part of CSS. Today, the order in which conflicting styles apply is decided by selector specificity and source order. With @layer, you can group rules into layers: a rule in a lower-priority layer will win over a rule in a higher-priority layer, even if the selector in the higher layer carries more specificity. You can think of it as something like a z-index for the cascade.
A typical use case mirrors the layers of the ITCSS methodology:
/* Create the layers, in the desired order */
@layer reset, base, theme;
/* Append the CSS to each of the layers */
@layer reset {
/* Append to 'reset' layer */
}
@layer base {
/* Append to 'base' layer */
h1.title {
font-size: 5rem;
}
}
@layer theme {
/* Append to 'theme' layer */
h1 {
font-size: 3rem;
}
}
In this setup, the font-size declared in the theme layer for an h1 would override the declaration in base, regardless of specificity.
Firefox supports Cascade Layers natively; Chrome and Edge support it behind a flag, with full support expected in Chrome 99. That means you can experiment with the spec today, but reliable production use will have to wait until it lands in stable builds across browsers. Fallbacks are tricky, since there is no clean way to mimic layer behavior with plain CSS without duplicating stylesheets or waiting for a polyfill.
- CSS Cascading and Inheritance Level 5 (official specification)
- A Complete Guide to CSS Cascade Layers by Miriam Suzanne (CSS Tricks)
- Cascade Layers are Coming to Your Browser by Una Kravets (Chrome developer blog)
- The Future of CSS: Cascade Layers (CSS @layer) by Bramus van Damme
- Getting Started With CSS Cascade Layers by Stephanie Eckles
Subgrid for Nested Alignment
The original promise of CSS Grid was to align entire interfaces on one clean grid. In practice, that would force you to flatten your HTML selectively, breaking semantics. Subgrid, part of CSS Grid Layout Module Level 2, gives a nested grid the ability to inherit the track sizes of its parent grid on either the row or the column axis.
It is especially handy for aligning items that would otherwise need fixed heights. Captions of varying lengths under images, for example, can all line up by having the caption grid inherit its parent’s row tracks:
To use it, set the parent grid normally, then specify subgrid as the value for grid-template-columns or grid-template-rows on the nested element:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, auto);
}
.grid > figure {
display: grid;
grid-template-rows: subgrid;
}
.grid figcaption {
grid-row: 2;
}
See the Pen [Subgrid captions](https://codepen.io/michellebarker/pen/YzERyor) by Michelle Barker.
Subgrid has been in Firefox since 2019, but no other major browser shipped it for nearly three years. The Chromium team appears to be making progress; you can follow the implementation issue. Safari support remains uncertain. Treat subgrid as a progressive enhancement for now.
- CSS Grid Layout Module Level 2 (official specification)
- MDN Subgrid page
- “CSS Grid Level 2: Here Comes Subgrid”, Rachel Andrew
Scroll-Linked Animations in CSS
Scroll-triggered interactions usually mean loading a JavaScript animation library. The @scroll-timeline at-rule aims to move that ability into CSS. You provide a keyframes animation, define a @scroll-timeline, and attach it with the animation-timeline property, or via the animation shorthand.
A simple example:
/* Set up keyframe animation */
@keyframes slide {
to { transform: translateX(calc(100vw - 2rem)); }
}
/* Configure our scroll timeline. Here we're giving it the name `slide-timeline` */
@scroll-timeline slide-timeline {
source: auto; /* the scrollable element that triggers the scroll-linked animation (the document by default) */
orientation: vertical; /* the scroll orientation (vertical by default) */
scroll-offsets: 0%, 100%; /* an array of progress intervals in which the timeline is active */
}
/* Specify the keyframe animation and the scroll timeline */
.animated-element {
animation: 1s linear forwards slide slide-timeline;
}
Offsets can be element-based instead of scroll-position-based via scroll-offsets:
@scroll-timeline slide-timeline {
scroll-offsets: selector(#element) end 0, selector(#element) start 1;
}
The specification is still in editor’s draft and can only be tested in Chrome behind a flag. Expect the API to change before reaching recommendation. For complex orchestration, a JavaScript library is still a reasonable choice, but simple scroll-driven effects may soon need no third-party code.
- Scroll-linked Animations (official specification)
- MDN page
- “Practical Use Cases for Scroll-Linked Animations in CSS with Scroll Timelines”, Bramus Van Damme
Native CSS Nesting
Sass users know the convenience of placing one rule inside another. Native nesting is now part of the CSS Nesting Module, and its syntax is familiar if you come from a preprocessor background.
Target an h2 inside an element with a class of card:
.card {
color: red;
& h2 {
color: blue;
}
}
You can also nest pseudo-classes and pseudo-elements:
.link {
color: red;
&:hover,
&:focus {
color: blue;
}
}
That translates to this in today’s CSS:
.link {
color: red;
}
.link:hover,
.link:focus {
color: blue;
}
No browser supports native nesting yet, even behind a flag. You can try it today with PostCSS and the postcss-preset-env plugin.
- CSS Nesting Module
- “CSS Nesting, specificity and you”, Killian Valkhof
Where the Platform Is Heading
The common thread through these features is an emphasis on writing more maintainable, efficient CSS. Several borrow from ideas introduced by preprocessors, reducing the need to rely on those tools. More importantly, they push us toward a design philosophy that accepts the web’s inherent flexibility. Users arrive with wildly different devices, color preferences, motion settings, and assistive tech. Fixed “pixel-perfect” layouts are unattainable, but CSS can provide suggestions and let browsers adapt gracefully—a philosophy called “Intrinsic” or “New responsive” design by Jen Simmons and Una Kravets.
The central challenge is no longer technical feasibility. It’s about equipping a new generation of developers with the judgment to know which tools fit which situation, and how to keep the user at the center of every decision.




