:has(), :is() and :where()
Three pseudo-classes have changed the way modern CSS is written. :is() and :where() let you group long selector lists into short ones. :has() does something CSS could not do for over twenty years: it styles an element based on what is inside it or what comes after it.
In this lesson you will learn what each one does, how they affect specificity, and some practical uses of :has() that used to need JavaScript.
:is(): group selectors
Selectors often repeat the same parts. Suppose you want links inside the header, the footer and the sidebar to have no underline:
/* Without :is() */
header a,
footer a,
aside a {
text-decoration: none;
}
/* With :is() */
:is(header, footer, aside) a {
text-decoration: none;
}:is() takes a list of selectors and matches any element that matches one of them. It is especially helpful when the repetition is in the middle of a selector, such as article :is(h2, h3, h4) + p, which would otherwise need three full selectors.
:is() also has a forgiving selector list. If one selector inside it is invalid or not supported by the browser, only that one is skipped and the rest still work. In a normal comma-separated list, one invalid selector makes the browser throw away the whole rule.
:where(): the same, with zero specificity
:where() matches exactly like :is(). The only difference is specificity, which you learned about in Cascade, Specificity & Inheritance.
| Selector | Specificity | Why |
|---|---|---|
:is(#main, .card) p | 1 id, 0 classes, 1 element | Takes the most specific item in its list (#main) |
:where(#main, .card) p | 0 ids, 0 classes, 1 element | Everything inside :where() counts as zero |
The :is() row is a common surprise: even when it matches through .card, the whole selector still carries the weight of #main.
Zero specificity makes :where() perfect for default styles that should be easy to override, such as a reset or a base theme:
/* Defaults: easy to override */
:where(.prose) h2 {
color: #1e3a8a;
margin-block: 1.5em 0.5em;
}
/* This wins, even though it is a single class */
.title-plain {
color: inherit;
}Use :is() when you want normal specificity and :where() when you want the rule to lose to anything more specific.
:has(): style an element by what it contains
:has() is often called the "parent selector". .card:has(img) selects every .card that contains an image somewhere inside it. You can also use the combinators from Combinators & Pseudo-classes inside it:
.card:has(> img)selects cards with an image as a direct child.h2:has(+ p)selects an<h2>that is immediately followed by a paragraph.section:has(h2, h3)selects sections that contain an<h2>or an<h3>.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>:has() cards</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; }
.card {
padding: 12px;
margin-bottom: 12px;
border: 1px solid #cbd5e1;
border-radius: 10px;
}
/* Cards with a photo get a two-column layout */
.card:has(> img) {
display: grid;
grid-template-columns: 96px 1fr;
gap: 12px;
background: #f0f9ff;
}
.card img { width: 96px; border-radius: 6px; }
.card h2 { margin: 0; font-size: 1.1rem; }
/* A heading directly followed by a paragraph gets less space below */
.card h2:has(+ p) { margin-bottom: 4px; }
</style>
</head>
<body>
<article class="card">
<img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" alt="Cartoon avatar of a smiling person in a green top">
<div>
<h2>Farhan Sheikh</h2>
<p>Scored 92 runs in the inter-college final.</p>
</div>
</article>
<article class="card">
<h2>Practice cancelled</h2>
<p>No practice on Sunday because of the rain.</p>
</article>
</body>
</html>Before :has(), you would add a class such as .card--with-photo to the HTML by hand, and forget it half the time. Now the CSS reacts to the content itself.
Using :has() to react to state
:has() really shines with state pseudo-classes such as :checked, :focus and :user-invalid. A parent can change when something inside it changes, with no JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>:has() with state</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; }
.packing { max-width: 320px; padding: 12px; border: 2px solid #cbd5e1; border-radius: 10px; }
.packing ul { list-style: none; padding: 0; }
.packing li { padding: 6px; border-radius: 6px; }
/* Cross out packed items */
li:has(input:checked) {
text-decoration: line-through;
color: #64748b;
background: #f1f5f9;
}
/* The whole box turns green when the checklist has focus */
.packing:has(:focus-visible) { border-color: #16a34a; }
/* Show the hint only after at least one item is packed */
.hint { display: none; color: #16a34a; }
.packing:has(input:checked) .hint { display: block; }
</style>
</head>
<body>
<div class="packing">
<strong>Train trip to Kochi</strong>
<ul>
<li><label><input type="checkbox"> Tickets</label></li>
<li><label><input type="checkbox"> Phone charger</label></li>
<li><label><input type="checkbox"> Water bottle</label></li>
</ul>
<p class="hint">Good start! Keep packing.</p>
</div>
</body>
</html>Tick a few items. Each list item styles itself when its checkbox is checked, and the whole box shows the hint once any box is ticked. Use the Tab key and watch the border change when a checkbox has keyboard focus.
A few more patterns you will see in real projects:
/* A form field wrapper highlights when its input is invalid */
.field:has(input:user-invalid) { border-color: #dc2626; }
/* A gallery with four or more images uses smaller tiles */
.gallery:has(> :nth-child(4)) { grid-template-columns: repeat(4, 1fr); }
/* Stop the page scrolling behind an open dialog */
body:has(dialog[open]) { overflow: hidden; }Rules and limits
:has()works in all current browsers. To give older browsers a fallback, wrap rules in@supports selector(:has(a)) { ... }.- You cannot put a
:has()inside another:has(). - Pseudo-elements such as
::beforeare not allowed inside:has(),:is()or:where(). - Unlike
:is()and:where(), the list inside:has()is not forgiving. One invalid selector inside it makes the whole rule invalid. :has()gets its specificity the same way as:is(): from the most specific selector in its list.
Common mistakes
- Expecting
:where()to win. A rule inside:where()has zero specificity, so almost anything overrides it. That is its purpose. - Forgetting that
:is()takes the highest specificity.:is(#id, p)gives every match id-level weight, which can make it hard to override. - Writing
:has()on the child.img:has(.card)looks for a card inside an image. Put:has()on the element you want to style:.card:has(img). - Using
:has()for things the HTML should say. If a card always has a photo, a normal class is clearer. Use:has()when the content or state really varies.
Key points
:is()groups selectors into one and uses the specificity of its most specific argument.:where()groups selectors too, but always has zero specificity, which is ideal for defaults.- Both
:is()and:where()have forgiving lists: an invalid selector inside is just skipped. :has()selects an element based on its descendants or following siblings, such as.card:has(> img)orh2:has(+ p).- Combine
:has()with:checked,:focus-visibleor:user-invalidto react to state without JavaScript. :has()cannot be nested and its selector list is not forgiving.
Exercise
Create an FAQ list where each question is a <details> element inside a <div class="faq">. Use :has() to give the whole FAQ box a blue border whenever any question is open (hint: details[open]). Then use :is() to give the <summary> text a bold style when it is hovered or has keyboard focus, in a single rule.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FAQ</title>
<style>
.faq { padding: 12px; border: 2px solid #cbd5e1; }
.faq:has(details[open]) { border-color: #2563eb; }
summary:is(:hover, :focus-visible) { font-weight: bold; }
</style>
</head>
<body>
<div class="faq">
<details><summary>When does the Jaipur batch start?</summary><p>On 3 November.</p></details>
<details><summary>Are weekend classes available?</summary><p>Yes, on Saturdays.</p></details>
</div>
</body>
</html>.faq:has(details[open]) matches the box only while a question is open. summary:is(:hover, :focus-visible) replaces two separate selectors with one.
Quick Quiz
1. What is the specificity of :where(#nav, .menu) a?
Everything inside :where() counts as zero, so only the a adds specificity.
2. Which selector matches an <h3> that is immediately followed by a <ul>?
:has(+ ul) checks the next sibling. h3 + ul would select the list, not the heading.
3. What happens if one selector inside :is() is invalid?
:is() and :where() use forgiving selector lists. :has() does not.
4. Why might you choose :where() for a set of base styles?
Base styles should lose to component styles, and :where() guarantees that by adding no specificity.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
