THE BROWSER

:has() let CSS style upward after twenty years

:has() removed a whole category of JavaScript by letting a selector look downward and style upward. It landed across engines in December 2023.

Hands on a keyboard, close, screen behind showing a selector highlighted in devtools
The selector that looks downward and styles upward, and the JavaScript it retired.

What it actually does

CSS selectors have always moved in one direction. You pick an element, you style it, and nothing further down the tree can change that decision. For decades this was simply the rule — and for decades, the most-requested feature in CSS was a way to break it. A parent selector. Something that could say: style this element if it contains that element.

Tailwind CSS logo with blue wave icon and white wordmark on black background
Utility frameworks added variants for it within months, which is how you know it mattered.Photo: Tailwind CSS logo with light text · Wikimedia Commons

The request was old enough to be a cliché. Browser vendors knew what was being asked. The problem was performance: a selector that could look down the tree before styling up it threatens to invalidate large portions of the cascade on every DOM change, forcing continuous re-evaluation. The concern was real enough to keep the feature out of browsers for the entire era of jQuery, the entire era of responsive design, and most of the era of component frameworks.

What changed was implementation strategy. The answer was not to relax the constraint but to handle it smarter — to scope the evaluation and limit what the browser had to recheck. Chrome shipped :has() in version 105, released August 2022. Safari had already shipped it in Safari 15.4 in March 2022. Firefox arrived last, shipping in version 121 in December 2023, the move that made :has() available across all major engines. That December date is the one that matters in practice, because it is when you could start writing it without a fallback plan.

  1. Safari 15.4March 2022, first major browser to ship :has()
  2. Chrome 105August 2022, second major browser
  3. Firefox 121December 2023, last major engine; full cross-browser availability from this date
  4. W3C classificationrelational pseudo-class, Selectors Level 4 specification

The category of JavaScript it replaced

The phrase "a whole category of JavaScript" is not an exaggeration. Any pattern where a class was toggled on a parent based on the presence or state of a child was, before :has(), a JavaScript problem. A form that needed to look different when any of its inputs held focus. A card that needed a different background when its image was missing. A navigation item that needed an active style when it contained a link matching the current URL. All of these required an event listener, a DOM query, and a class mutation — or a framework to manage all three.

:has() handles each of those in a rule. form:has(input:focus) applies when any input inside the form is focused. li:has(> a.active) catches the navigation case. The child combinator > inside :has() keeps scope tight. And because the styling logic is now declarative and in the stylesheet, it moves with the component, survives framework rewrites, and costs the browser nothing extra at runtime that a comparable JavaScript approach would not have cost.

That December date is the one that matters in practice, because it is when you could start writing it without a fallback plan.

The W3C Selectors specification, maintained by the CSS Working Group, classifies :has() as a relational pseudo-class — the name is precise. It expresses a relationship between elements rather than a state of one. Grouping it with :focus or :hover in your mental model will cause confusion; grouping it with structural selectors like :nth-child() gets you closer.

A browser window showing a grid overlay across a laid-out page, devtools open beside it
Twenty years of asking, shipped quietly, and mostly used for form states.

What it enables at the page level

The most legible use cases are component-level: toggle states, conditional layouts inside a card, form feedback. But :has() also works from high in the document tree, which opens layout decisions that previously lived entirely in JavaScript. body:has(dialog[open]) lets you alter the entire page when a modal is present — suppress scroll, dim content — without touching a class on the body element in your script. article:has(> figure:first-child) lets you offset the text differently when an article opens with an image, making the layout responsive to content rather than to a content-management field.

That last point is the underlying shift. Before :has(), a layout that needed to know about its contents required either server-side knowledge or client-side JavaScript to pass that knowledge upward. Now a single rule holds the logic, and the browser evaluates it the same way it evaluates everything else in the cascade.

Rachel Andrew, who has written extensively about the CSS specification process, has noted the implementation work that made this possible — the selector needed to be fast in practice, not just sound on paper.

Twenty years is a long time to wait for a feature. The payout is proportionate.