The CSS :has Selector (and 4+ Examples)

Avatar of Robin Rendle
Robin Rendle on (Updated on )

The CSS :has selector helps you select elements that contain elements that match the selector you pass into the :has() function. It’s essentially a “parent” selector, although far more useful than just that. For example, imagine being able to select all <div>s but only when they contain a <p>. That’s what we can do:

div:has(p) {
  background: red;
}
/*
  <div> <!-- selected! -->
     <p></p>
  <div>

  <div></div> <!-- not selected -->
  <div> <!-- not selected -->
    <section></section>
  </div>
*/

Although it’s not supported in any browser as I write, it has now dropped in Safari Technical Preview 137, so it’s starting to happen!

Pretty darn handy right! Here’s another example. Say you want space after your headers. Of course! A bit of margin-block-end on your h2 should do it. But… what if there is a subtitle? Now we can select a parent on the condition a subtitle is present and adjust the spacing.

h2,
.subtitle {
  margin: 0 0 1.5rem 0;
}
.header-group:has(h2):has(.subtitle) h2 {
  margin: 0 0 0.2rem 0; /* reduce spacing on header, because subtitle will handle it */
}

/*
  <div class="header-group">
    <h2>Blog Post Title</h2> <!-- main spacing applied here -->
  </div>

  <div class="header-group">
    <h2>Blog Post Title</h2>
    <div class="subtitle"> <!-- main spacing applied here -->
      This is a subtitle
    </div>
  </div>
*/
The CSS :has selector being helpful in spacing headers with subtitles (or not).
On the left, the main spacing happens after the h2, on the right, the main spacing happens after the subtitle.

The way I think about :has is this: it’s a parent selector pseudo-class. That is CSS-speak for “it lets you change the parent element if it has a child or another element that follows it.” This might feel weird! It might break your mental model of how CSS works. This is how I’m used to thinking about CSS:

.parent .child {
  color: red;
}

You can only style down, from parent to child, but never back up the tree. :has completely changes this because up until now there have been no parent selectors in CSS and there are some good reasons why. Because of the way in which browsers parse HTML and CSS, selecting the parent if certain conditions are met could lead to all sorts of performance concerns.

If I sit down and think about all the ways I might use :has today, I sort of get a headache. It would open up this pandora’s box of opportunities that have never been possible with CSS alone.

Another example: let’s say we want to only apply styles to links that have images in them:

a:has(> img) {
  border: 20px solid white;
}

This would be helpful from time to time. I can also see :has being used for conditionally adding margin and padding to elements depending on their content. That would be neat.

The :has selector is part of the CSS Selectors Level 4 specification which is the same spec that has the extremely useful :not pseudo-class.


You could argue that the CSS :has selector is more powerful than just a “parent” selector, which is exactly what Bramus has done! Like in the subheadings example above, you aren’t necessarily ultimately selecting the parent, you might select the parent in a has-condition, but then ultimately select a child element from there.

/*  Matches <figure> elements that have a <figcaption> as a child element */
figure:has(figcaption) { … }

/* Matches <img> elements that is a child of a <figure> that contains a <figcaption> child element */
figure:has(figcaption) img { … }

There you can quickly see that the second selector is selecting a child <img>, not just the parent of the <figcaption>.

Selector List

You can chain it:

article:has(h2):has(ul) {

}

Or give it a selector list:

article:has(h2, ul) {

}

And the list is forgiving: The list is no longer “forgiving” after the W3C adopted a resolution in December 2020 in response to a reported issue. So, if the selector list contains even one invalid argument, the entire list is ignored:

/* 👎 */
article:has(h2, ul, ::-blahdeath) {
  /* ::blahdeath is invalid, making the entire selector invalid. */
}

A workaround is to nest a more forgiving selector in there, such as :is() or :where():

/* 👍 */
article:has(:where(h2, ul, ::-blahdeath)) {
  /* :where is a forgiving selector, making this valid. */
}

Testing for Support

@supports(selector(:has(p))) {
  /* Supported! */
}

The :not() selector is part of the same spec…

Unlike :has, :not does have pretty decent browser support and I used it for the first time the other day:

ul li:not(:first-of-type) {
  color: red;
}

That’s great I also love how gosh darn readable it is; you don’t ever have to have seen this line of code to understand what it does.

Another way you can use :not is for margins:

ul li:not(:last-of-type) {
  margin-bottom: 20px;
}

So every element that is not the last item gets a margin. This is useful if you have a bunch of elements in a card, like this:

… and also :is() and :where()

CSS Selectors Level 4 is also the same spec that has the :is selector that can be used like this today in a lot of browsers:

:is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* ... which would be the equivalent of: */
section h1, section h2, section h3, section h4, section h5, section h6, 
article h1, article h2, article h3, article h4, article h5, article h6, 
aside h1, aside h2, aside h3, aside h4, aside h5, aside h6, 
nav h1, nav h2, nav h3, nav h4, nav h5, nav h6 {
  color: #BADA55;
}

More Info

So that’s it! :has should be quite useful to use soon, and its cousins :is and :not can be fabulously helpful already and that’s only a tiny glimpse — just three CSS pseudo-classes — that are available in this new spec.