# Modern Selectors: :is(), :where() and :has()

The :is() and :where() pseudo-classes shorten long selector lists, :where() with no specificity at all, and :has() styles an element by what it contains, so a page can react to a click with no JavaScript.

Journeyman · CSS · Updated November 1, 2026

## After this lesson you can

- Shorten a selector list with :is() and write a zero-specificity default with :where()
- Style an element by what it contains with :has(), such as a row whose checkbox is checked

## Key takeaways

- `:is(header, main, footer) a` means the same as three selectors in a list: a link in the header, in main or in the footer.
- `:is()` takes the specificity of its most specific selector. `:where()` matches the same way but counts zero, so any later rule can override it.
- `:has()` styles an element by what's inside it: `li:has(:checked)` is a song whose checkbox is ticked. It's often called the parent selector.
- `:has()` works in every current major browser. It is Baseline widely available since June 2026.

For years, the most-asked CSS question was "how do I style a parent based on its child?" Highlight a song's whole row when its heart is ticked; gray out a form's Send button while a field is wrong. The answer used to be "you can't: use JavaScript". Now it's `:has()`. This lesson teaches it, with its two cousins `:is()` and `:where()`, which make long selectors short and give you defaults that any later rule can override. They are three of the newest tools in CSS, and they turn a static page into one that answers back.

## Pseudo-classes that take a list

All three are **functional pseudo-classes**: pseudo-classes with parentheses, like `:not()` from *Pseudo-Classes: :hover, :focus-visible, :nth-child() and More*. Inside the parentheses goes a list of selectors, separated by commas.

The examples use the *Road Trip Mix* page, the playlist this chapter styles. Here are its header (with a skip link added), a liked song and its footer, cut down:

```html
<header>
  <h1>Road Trip Mix</h1>
  <a href="#songs">Skip to the songs</a>
</header>
<main>
  <ol>
    <li>
      <strong>Windows Down</strong>
      <span>Maple Avenue</span>
      <input type="checkbox" aria-label="Like Windows Down" checked>
    </li>
  </ol>
</main>
<footer>
  <a href="road-trip-mix-lyrics.pdf">Lyrics for every song (PDF)</a>
</footer>
```

## :is(): a long list, made short

Say every link on the page should turn orange on hover, but only in the header, the main part and the footer. As a selector list, that's:

```css
header a:hover,
main a:hover,
footer a:hover {
  color: #ea580c;
}
```

**`:is()`** (read "is") takes the part that changes and says it once:

```css
:is(header, main, footer) a:hover {
  color: #ea580c;
}
```

Read it as "a hovered link inside something that is a header, a main or a footer". It matches exactly the same links. There's a bonus: in a plain list, one selector the browser can't read throws away the whole rule, while `:is()` skips that one selector and keeps the rest.

`:is()` takes the specificity of the *most specific* selector in its list. `:is(header, #top) a` counts as an id, (1, 0, 1), even for links in the header. Specificity is the scoring from *Specificity: Scoring a Selector*.

## :where(): the same, but it counts zero

**`:where()`** (read "where") matches exactly like `:is()`, with one difference: its specificity is always zero, whatever is inside it. That makes it perfect for **defaults**: styles that should apply unless something else says otherwise.

```css
:where(footer) a {
  color: #475569;
}

a {
  color: #7c3aed;
}
```

Both rules match the footer's lyrics link. `:where(footer) a` scores only the `a`, (0, 0, 1), exactly like the plain `a`, so they tie and the later rule wins: the link is purple. Write `:is(footer) a` instead and it scores (0, 0, 2) and wins, so the link stays gray. Libraries and resets wrap their selectors in `:where()` so that your own rules beat theirs without a fight.

**Try it.** Put both rules in your `road-trip.css`, with a footer link in `road-trip.html`, and check the link's color. Change `:where` to `:is` and check again. In the developer tools, hover over each selector in the Styles pane to see its specificity.

## :has(): style an element by what's inside it

**`:has()`** (read "has") matches an element that *contains* something matching the selector in its parentheses. Because it styles the outer element, the parent, it's often called the **parent selector**.

```css
li:has(:checked) {
  background-color: #fce7f3;
}
```

Read it as "a song that has something checked inside it". Tick the heart on any song, and its whole row turns pink at once; untick it, and the pink goes. The checkbox sits inside the `<li>`, and before `:has()`, CSS could style the checkbox but never the row around it.

`:has()` works in every current major browser: Chrome and Edge since version 105, Safari since 15.4 and Firefox since 121 (December 2023). It is **Baseline widely available** since June 2026, which means it has worked in all of them for over two and a half years. Like `:is()`, it takes the specificity of its most specific selector: `li:has(:checked)` is (0, 1, 1).

## Three jobs for :has()

- **A card with a picture.** `article:has(img)` gives cards that have a photo a different look from cards that don't, from the same HTML.
- **A form with a problem.** `form:has(:invalid) button` dims the Send button while any field breaks its own rules. `:invalid` is a pseudo-class for a form field whose value isn't allowed, such as a required box left empty or an email address with no @.
- **A row whose checkbox is checked.** `li:has(:checked) strong` reaches the title of every liked song, so it can turn bold or change color.

```css
form:has(:invalid) button {
  opacity: 0.5;
}

li:has(:checked) strong {
  color: #be185d;
}
```

You can combine `:has()` with everything in this chapter: `li:has(> abbr)` is a song with an explicit tag directly inside it, and `li:not(:has(:checked))` is a song nobody has liked yet.

![Two playlist rows side by side: the unliked song is white with an empty checkbox; the liked song has a ticked pink checkbox, and its whole row is pink with a dark pink title](https://fsdtrainingmedia.blob.core.windows.net/public/images/2026/10/f3af8c3a687c4bdb8b36b3cbc46cdad1.webp)

## Pages that answer back, with no JavaScript

A checkbox plus `:has()` is a small switch that CSS can read. People use it for a recipe whose ingredients cross themselves out as they're ticked, a "show only liked songs" filter, a menu that opens, and a dark-mode switch. JavaScript is still the right tool when a page must remember a choice after it's closed or talk to a server. For a look that changes with a click, CSS can now do it alone.

## Check yourself

2 questions to try on the lesson page.

## So what

`:is()` and `:where()` keep selectors short and defaults out of the way, and `:has()` lets a page respond to what people do, right in the stylesheet. Many interactive touches that once needed a script now take one rule.

## Key terms

- [:is()](https://www.ask-codey.com/glossary/is)
- [:where()](https://www.ask-codey.com/glossary/where-css)
- [:has()](https://www.ask-codey.com/glossary/has)
- [Functional pseudo-class](https://www.ask-codey.com/glossary/functional-pseudo-class)

## Cheat sheets

- [Modern Selectors: :is(), :where() and :has()](https://www.ask-codey.com/cheat-sheets/css-is-where-has-cheat-sheet)

## Related

- [Pseudo-Classes: :hover, :focus-visible, :nth-child() and More](https://www.ask-codey.com/lessons/css-pseudo-classes)
- [Specificity: Scoring a Selector](https://www.ask-codey.com/lessons/css-specificity)
- [Exercise: Style a Playlist Without Adding Classes](https://www.ask-codey.com/lessons/exercise-css-playlist-without-classes)

Source: https://www.ask-codey.com/lessons/css-is-where-has
