Pure CSS content filtering with :has()

I was able to build a content page that has filter buttons with 0 line of JavaScript and without compromise on the DOM structure. See it in action on my "Talks" website or see a recording:

And here is a live demo:

  1. My podcast
  2. My talk
  3. My talk 2
  4. My podcast 2

Understanding the code

CSS has now become so powerful that it's possible to style an element based on the state of any other element on the page. In the past, it was only possible to do so for children or siblings.
This new power comes from a new selector: :has().

Let's be more concrete. Consider this HTML:

<body>
  <div class="filters">
    <input type="checkbox" id="talks-filter" checked>
    <label for="talks-filter">Talks</label>
    <input type="checkbox" id="podcasts-filter" checked>
    <label for="podcasts-filter">Podcasts</label>
  </div>

  <main>
    <ol>
      <li class="podcast">My podcast</li>
      <li class="talk">My talk</li>
      <li class="talk">My talk 2</li>
      <li class="podcast">My podcast 2</li>
    </ol>
  </main>
</body>

We want to hide/display the list items (<li>) depending on which checkbox (<input type="checkbox">) is checked.

The only DOM relationship between the list and the checkboxes is that they are part of the same document.
But that's not a problem anymore: By using the CSS selector :has() on the <body> element, we can basically check for the state of any element in the document, and style any other element accordingly.

Giving a filtering behavior to our checkbox is as easy as using these two selectors of CSS:

body:has(#talks-filter:not(:checked)) .talk {
  display: none;
}
body:has(#podcasts-filter:not(:checked)) .podcast {
  display: none;
}

Alright, what is this doing exactly? Let's de-construct the first selector:

  1. display: none; will simply hide our elements is they match the selector. You probably know that.
  2. .talk will simply select any element with a class="talk" attribute. Again, you probably know that.
  3. #talks-filter:not(:checked) will select the element with id="talks-filter" that doesn't (:not()) have the checked attribute.
  4. body:has(#talks-filter:not(:checked)) will match the <body> element if it doesn't contain any checked id="talks-filter" element.

When we arrange all of together: We hide any element that has the class "talk" attribute if it a child of a <body> element that doesn't contain any checked id="talks-filter" element.

In other words: if the "talks" checkbox is unchecked, it will hide all talks.

Can I Use?

As you can read, the CSS :has() selector is still behind a flag in Chrome 103. the feature is planned to be enabled by default in Chrome 105.

So, what do we do if the user's browser doesn't support :has()? Well, we can check for the support of this feature with @supports(selector(:has(*))) and gracefully degrade our page by not displaying the checkbox filters, or in my case, by hiding the <div class="filters"> element entirely:

menu {
  display: none;
}

@supports(selector(:has(*))) {
  menu {
    display: block;
  }
}

I think we are only scratching the surface of how powerful the :has() selector is. I predict the community will use it to implement many declarative "pure CSS" logic that previously required JavaScript code (or even loading a JavaScript framework).

If you are running a supported browser, see the code in action on my "Talks" website.