:has() finally lets CSS style an element based on what it contains, replacing many JavaScript class toggles.
Three functional pseudo-classes changed how CSS is written.
:is() takes a selector list and matches any of them: :is(h1, h2, h3) a instead of three selectors. Its specificity is that of its most specific argument, so :is(#id, p) counts as an id everywhere.
:where() works the same way but always has zero specificity. It's perfect for defaults and resets that should be easy to override: :where(ul, ol)[role="list"] { list-style: none; }.
:has() is the long-awaited relational selector. A:has(B) matches A if B matches relative to it:
.card:has(img)is a card that contains an image (a "parent selector").h2:has(+ p)is an h2 immediately followed by a p (look forwards from a sibling).form:has(:user-invalid)is a form with an invalid field, to disable its submit style.body:has(dialog[open])locks page scrolling while a modal is open.label:has(input:checked)styles the label of a checked input.
:has() takes the specificity of its most specific argument, like :is(), and it can't be nested inside another :has().
Browsers optimise these well, but avoid extremely broad patterns like *:has(*) on large documents. With :has(), :is() and :where(), many state classes that JavaScript used to toggle become plain CSS.
Example
/* group without repetition */
:is(article, aside) :is(h2, h3) { margin-block-start: 1.5em; }
/* zero-specificity default, easily overridden by any class */
:where(button) { font: inherit; cursor: pointer; }
/* card layout changes when it contains an image */
.card:has(> img) { display: grid; grid-template-columns: 120px 1fr; }
/* style a label whose checkbox is checked */
label:has(input:checked) { font-weight: 600; }
/* highlight the whole form row with an invalid field */
.field:has(input:user-invalid) { border-left: 3px solid crimson; }
/* no page scroll while a modal is open */
body:has(dialog[open]) { overflow: hidden; }Remember
- :is() groups selectors and takes the highest specificity of its arguments
- :where() groups with zero specificity
- :has() selects an element by its descendants or following siblings
Watch out for
- Putting an id inside :is() and accidentally raising the specificity of the whole selector.