:has() CSS Selector : A Powerful Tool for Complex Styling

The :has() selector styles an element based on what it contains or what follows it: parent selection, form state, sibling rules. Examples, support, and limits.

:has() CSS Selector : A Powerful Tool for Complex Styling

CSS selectors always looked down the tree: a parent could style its children, never the other way round. "Style this form if it contains an invalid field" needed JavaScript. The :has() pseudo-class removes that limit. It matches an element based on what is inside it, or what comes after it, and since June 2026 it is Baseline widely available, meaning every major browser has supported it for more than two and a half years.

Syntax

/* an element that contains something matching the argument */
article:has(img) { display: grid; }

/* an element followed by something */
h2:has(+ p) { margin-bottom: 0.25em; }

/* direct children only */
ul:has(> li.active) { border-color: var(--accent); }

The argument is a relative selector list: it is evaluated from the element on the left, so combinators at the start, >, + and ~, are allowed. The element that gets styled is always the one before :has().

Parent selection

The classic case. A card with an image lays out differently from a card without one:

.card { padding: 20px; }
.card:has(img) { padding: 0; }
.card:has(img) .card-body { padding: 20px; }

Before :has() this needed a modifier class set by the template or by JavaScript, and it broke the moment someone added an image through a CMS.

Form state without scripts

/* highlight the whole field group when its input is invalid */
.field:has(:invalid) { outline: 2px solid #b91c1c; }

/* disable the submit button visually until the form is valid */
form:has(:invalid) button[type="submit"] { opacity: .5; pointer-events: none; }

/* style a label when its checkbox is checked */
label:has(input:checked) { font-weight: 700; }

Combine it with :user-invalid instead of :invalid and the error state appears only after the user has interacted with the field, which is what people expect.

Sibling and quantity rules

/* a heading directly followed by a subtitle gets less space below */
h1:has(+ .subtitle) { margin-bottom: 0; }

/* a grid with four or more items switches to a denser layout */
.gallery:has(:nth-child(4)) { grid-template-columns: repeat(4, 1fr); }

/* a nav item that contains a dropdown */
nav li:has(ul) > a::after { content: " ▾"; }

The quantity query trick, counting children with :nth-child(), used to need the "lobotomized owl" family of hacks. With :has() it is one readable line.

Page-level state

/* lock scrolling while a dialog is open */
html:has(dialog[open]) { overflow: hidden; }

/* dim the page when any element inside has focus-visible within a menu */
body:has(.menu:focus-within) .backdrop { opacity: 1; }

This is the pattern that replaces the most JavaScript: toggling a class on body because something deep in the page changed.

Limits and performance

  • :has() cannot be nested inside another :has(), and it cannot contain pseudo-elements such as ::before.
  • Its specificity is that of its most specific argument, so .card:has(img) is (0,2,0), not (0,1,0). Keep that in mind when a later rule stops winning.
  • Browsers invalidate :has() rules efficiently for normal use. The expensive forms are the broad ones: *:has(*) or :has() at the root of a very large, frequently mutating DOM. Scope the left side to an element or class and the cost disappears.

Browser support

Browser compatibility table for the :has() selector: Chrome 105, Edge 105, Firefox 121, Opera 91, Safari 15.4, and the matching Android and iOS versions

Safari 15.4 shipped it in March 2022, Chrome and Edge 105 in August 2022, Firefox 121 in December 2023, which made it Baseline newly available. On 19 June 2026 it reached widely available. If your analytics still show a meaningful share of Firefox 120 or older, keep a class-based fallback for the critical states; otherwise use it freely.

Reference: :has() on MDN and its Web Platform Status entry.

Frequently asked questions

Is :has() safe to use in production?

Yes. It is Baseline widely available since June 2026: Chrome 105, Edge 105, Safari 15.4 and Firefox 121 all support it, and it has had 30 months of support across all of them.

Does :has() hurt performance?

Browsers optimize it well for the common cases. Avoid very broad forms such as :has() on huge pages; scope the left side to a class or element and keep the argument simple.

Can I nest :has() inside :has()?

No. :has() cannot contain another :has(), and it cannot contain pseudo-elements. Everything else from the selector language is allowed, including combinators and :not().