Filtering Content With the CSS :has() Selector

The CSS :has() selector can do more than style a parent. It can also connect the state of a form control to content elsewhere on the page, which is enough to build a small filter without JavaScript.

Here is a list split into two teams, with one checkbox for each team:

<main>
  <div class="filters">
    <input type="checkbox" id="a-team-filter" checked>
    <label for="a-team-filter">Team A</label>

    <input type="checkbox" id="b-team-filter" checked>
    <label for="b-team-filter">Team B</label>
  </div>

  <ol>
    <li class="a-team">John</li>
    <li class="b-team">Robert</li>
    <li class="b-team">Donald</li>
    <li class="a-team">Rupert</li>
  </ol>
</main>

Let the checkbox state control the list

The useful part is only four lines of CSS:

body:has(#b-team-filter:not(:checked)) .b-team {
  display: none;
}

body:has(#a-team-filter:not(:checked)) .a-team {
  display: none;
}

Read the first selector from left to right: when the body contains an unchecked #b-team-filter, hide every element with the .b-team class. The second rule does the same for Team A.

This is a nice use of :has() because the checkboxes and results do not need to be adjacent. The common ancestor provides the connection.

There is a trade-off. CSS can change what is visible, but it does not give you the state management, URL parameters, announcements, or more complex matching that a proper JavaScript filter can provide. I would use this for a small optional filter, not for the main search interface on a large site.

You can try the complete CodePen demo.