Modern CSS Features

Container queries, the :has() selector, clamp()/min()/max() revisited, and cascade layers with @layer.

Container queries: styling based on a parent, not the viewport

A media query only ever knows about the viewport. That's a real limitation for a reusable component — a card might live in a wide main column on one page and a narrow sidebar on another, and "the viewport is 1200px wide" tells you nothing about how much space that specific card actually has. Container queries solve exactly this: they let an element respond to the size of its own containing element, wherever that element happens to sit.

Css
.card-container {
    container-type: inline-size; /* this element becomes a "query container" based on its own width */
    container-name: card;
}

.card {
    display: block;
}

@container card (min-width: 400px) {
    .card {
        display: flex;
        gap: 1rem;
    }
}
HTML
<div class="card-container" style="width: 300px;">
    <div class="card"><!-- stacked — container is under 400px --></div>
</div>

<div class="card-container" style="width: 600px;">
    <div class="card"><!-- side-by-side — same component, wider container --></div>
</div>

The identical .card markup lays out differently purely based on the space its container has, regardless of the viewport's total width — genuinely new behavior a media query alone can never express, since a media query has no way to know how wide one particular ancestor element happens to be.

:has() — the "parent selector"

For as long as CSS has existed, a selector could only describe a relationship going down the DOM tree (a descendant, a child) or sideways (a sibling) — never upward. :has() is the first native CSS selector that lets a rule match a parent based on what it contains:

Css
/* Style a form field wrapper differently once its input is invalid */
.form-group:has(input:invalid) {
    border-color: #ef4444;
}

/* Give a card different layout only when it actually contains an image */
.card:has(img) {
    grid-template-rows: auto 1fr;
}

/* Select a link that directly wraps an image, but not a link wrapping plain text */
a:has(> img) {
    border: none;
}

Before :has(), any of these required JavaScript to inspect an element's children and toggle a class in response — :has() moves that logic into a single, declarative CSS rule that updates live as the DOM changes, no script involved at all.

clamp(), min(), and max() — revisited

The responsive design page in this track introduced clamp() for fluid font sizing. The same three functions compose well beyond that one use case, and it's worth seeing min() and max() used on their own, since clamp(a, b, c) is really just shorthand for max(a, min(b, c)):

Css
/* min() picks whichever value is SMALLER — a practical "don't exceed" cap */
.sidebar {
    width: min(300px, 100%); /* never wider than 300px, but shrinks below that on narrow screens */
}

/* max() picks whichever value is LARGER — a practical "never shrink below" floor */
.hero-title {
    font-size: max(2rem, 5vw); /* scales up on wide screens, never drops below 2rem on narrow ones */
}

/* clamp(min, preferred, max) combines both into one fluid value with a hard floor and ceiling */
.container {
    padding-inline: clamp(1rem, 5vw, 3rem);
}

All three also accept full expressions with mixed units, including calc()-like arithmetic directly inside them:

Css
.card {
    width: min(90%, 40rem);
    margin-inline: max(1rem, calc((100% - 40rem) / 2));
}

@layer — explicit cascade ordering

Without @layer, two conflicting rules resolve purely by specificity and source order — which means a component's own CSS and a later utility-class override are effectively fighting on the framework's terms, not yours. Cascade layers let you declare, up front, which whole groups of rules should always lose to which other groups — regardless of how specific any individual selector inside them is:

Css
@layer reset, base, components, utilities;

@layer reset {
    * { margin: 0; padding: 0; box-sizing: border-box; }
}

@layer base {
    h1 { font-size: 2rem; font-weight: 700; }
}

@layer components {
    .btn { padding: 0.5rem 1rem; border-radius: 6px; background: #e5e7eb; }
}

@layer utilities {
    .bg-blue { background: #3b82f6; } /* a single class selector, low specificity... */
}
HTML
<button class="btn bg-blue">Save</button>

.bg-blue here has lower specificity than .btn, yet it still wins, because utilities is declared as a later layer than components — layer order overrides specificity entirely for any rules that belong to a named layer. This is exactly the mechanism that solves the recurring real-world problem of "why doesn't my utility class override this component class" without reaching for !important at all.

Common mistakes

  • Setting container-type on the same element you're writing the @container query for — a container query targets descendants of the element marked as a container, not the container element itself.
  • Forgetting that :has() support requires a genuinely modern browser — while every major evergreen browser supports it as of 2026, any project still needing to support noticeably older browser versions should verify support before relying on it for anything load-bearing.
  • Writing clamp(3rem, 1rem, 5vw) with the arguments in the wrong conceptual order — clamp() expects (minimum, preferred, maximum), and mixing up the minimum/maximum can silently produce a value that never changes across viewport sizes.
  • Declaring styles outside of any @layer block on the same stylesheet as layered rules — un-layered styles always take priority over any layered style, regardless of specificity, which surprises people expecting layers to fit into the normal cascade rather than sitting above it entirely.