01The grid you've been waiting for
CSS Grid matured quietly, and a lot of developers are still reaching for flex hacks and JavaScript resize listeners out of habit. The core of Grid was always solid — grid-template-columns, grid-template-rows, named areas — but the piece that changes daily work is subgrid. Once you declare a grid on a parent, child elements can now opt into that same track system with grid-template-columns: subgrid. Cards inside a card grid can align their internal parts — headings, body copy, footers — to the parent's columns without a single line of JS measuring offsetHeight and nudging things into place. Firefox shipped it first; it's now in all evergreen browsers.
The repeat(auto-fill, minmax()) pattern has been around long enough to feel boring, but plenty of production codebases still have JavaScript calculating how many columns fit a container. They shouldn't. grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)) hands that arithmetic entirely to the browser, reflows correctly on resize, and costs nothing at runtime.
Firefox shipped it first; it's now in all evergreen browsers.
02Container queries are the real shift
Media queries always had an awkward truth at their centre: they respond to the viewport, not to the element that actually needs to change. A card component dropped into a narrow sidebar and the same card dropped into a wide main area had no way to style themselves based on their own available space — you were either writing context-specific overrides or reaching for a JavaScript ResizeObserver to add conditional classes.
Container queries fix this properly. Mark a parent as a container:
.card-wrapper {
container-type: inline-size;
container-name: card;
}Then query against it from the child:
@container card (min-width: 30rem) {
.card { flex-direction: row; }
}The card now responds to its container, not the viewport. Ship one component; it adapts everywhere. This is the pattern framework authors have been faking with JavaScript for years. Browser support landed across Chrome, Firefox and Safari in 2023, so there's no credible reason to avoid it in new projects.
03:has() — the selector nobody thought CSS would get
The old rule was: CSS selectors move down the tree, never up. :has() broke it. It lets you style a parent — or a preceding sibling — based on what it contains.
/* highlight a form group when its input is invalid */
.form-group:has(input:invalid) {
border-color: red;
}
/* only show the label when an input has a value */
label:has(+ input:not(:placeholder-shown)) {
opacity: 1;
}Before :has(), both of these patterns needed event listeners, class toggling, and DOM queries. The second one — showing a floating label only when a field has content — was a genuine frontend interview staple, answered almost exclusively with JavaScript. Now it's one selector. The performance story is also good: style recalculation in the browser is faster than a round-trip through a JS event handler adding and removing classes on every keystroke.
04Unlearning is the work
None of these features are exotic. Grid, container queries, and :has() all sit in the HTML5 Grew Up category of platform improvements that browsers shipped while the industry was busy debating framework choices. The patterns they replace — resize listeners, conditional class toggles, JavaScript-driven layout — aren't wrong so much as they're now unnecessary overhead.
