:is(), :where() and :has()

CSS · Advanced · 12 min

: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.

Check yourself

  1. What is the specificity of :where(#main .nav) a?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has