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
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) ameans 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:
<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:
header a:hover,
main a:hover,
footer a:hover {
color: #ea580c;
}
:is() (read "is") takes the part that changes and says it once:
: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.
: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.
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) buttondims the Send button while any field breaks its own rules.:invalidis 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) strongreaches the title of every liked song, so it can turn bold or change color.
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.

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.