CSS :has(), :is(), :where() and Nesting
Modern CSS has new selectors that once needed JavaScript or a tool like Sass. All of them work in every modern browser today.
:has(): The Parent Selector
For many years CSS could only look down the page tree: "style a link inside a card". It could not say "style a card that contains an image". :has() makes this possible.
Example
/* A card that contains an image */
.card:has(img) {
padding: 0;
}
/* A form that has an invalid field */
form:has(:user-invalid) button {
opacity: 0.5;
}Read A:has(B) as "A that has B inside it".
Example
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.option {
display: block;
padding: 12px;
margin-bottom: 8px;
border: 2px solid #dddddd;
border-radius: 8px;
cursor: pointer;
}
.option:has(input:checked) {
border-color: #264de4;
background-color: #eef2ff;
}
.plans:has(input:checked) .hint {
display: none;
}
</style>
</head>
<body>
<div class="plans">
<p class="hint">Please choose a class time:</p>
<label class="option"><input type="radio" name="time"> Morning (9 to 12)</label>
<label class="option"><input type="radio" name="time"> Evening (5 to 8)</label>
<label class="option"><input type="radio" name="time"> Weekend</label>
</div>
</body>
</html>Try it Yourself »The selected box gets a blue border, and the hint disappears, all with no JavaScript.
:has() can also look at the next sibling: h2:has(+ p) selects an <h2> that is directly followed by a <p>.
:has() is powerful, but very broad rules like body:has(...) make the browser do more work. Keep the selector inside :has() short and specific.
:is(): Shorter Selector Lists
:is() takes a list of selectors and matches any of them. It saves a lot of repeating.
Example
/* Old way */
article h1,
article h2,
article h3 {
color: #264de4;
}
/* With :is() */
article :is(h1, h2, h3) {
color: #264de4;
}Another plus: if one selector in a normal comma list is invalid, the whole rule is ignored. Inside :is(), an invalid selector is just skipped and the rest still works.
:where(): The Same, but With Zero Specificity
:where() matches exactly like :is(). The only difference is specificity:
| Selector | Specificity |
|---|---|
:is(#nav, .menu) a | Takes the strongest item inside: here the id, so 1-0-1 |
:where(#nav, .menu) a | Everything inside :where() counts as 0, so 0-0-1 |
This makes :where() perfect for default styles that should be easy to override:
Example
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
/* Default link style with zero specificity */
:where(.content) a {
color: #264de4;
text-decoration: underline;
}
/* A simple class easily overrides it */
.btn {
color: white;
background-color: seagreen;
text-decoration: none;
padding: 6px 12px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="content">
<p>Read our <a href="#">admission rules</a> first.</p>
<p><a class="btn" href="#">Apply now</a></p>
</div>
</body>
</html>Try it Yourself »Without :where(), the rule .content a (0-1-1) would beat .btn (0-1-0) and the button text would be blue.
Native CSS Nesting
Nesting lets you write rules inside other rules, so related styles stay together. It was made popular by Sass, and now plain CSS can do it.
Example
.card {
padding: 16px;
border: 1px solid #dddddd;
h3 {
margin-top: 0; /* same as .card h3 */
}
& .price {
color: crimson; /* same as .card .price */
}
&:hover {
border-color: #264de4; /* same as .card:hover */
}
&.featured {
border-width: 3px; /* same as .card.featured */
}
@media (min-width: 600px) {
padding: 24px; /* .card on wider screens */
}
}The & means "the parent selector". You need & when there must be no space, such as &:hover or &.featured. For a child selector like h3, the & is optional.
Example
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.menu {
display: flex;
gap: 8px;
padding: 0;
list-style: none;
a {
display: block;
padding: 8px 14px;
border-radius: 6px;
color: #1f2937;
text-decoration: none;
&:hover {
background-color: #eef2ff;
}
&.active {
background-color: #264de4;
color: white;
}
}
}
</style>
</head>
<body>
<ul class="menu">
<li><a class="active" href="#">Home</a></li>
<li><a href="#">Courses</a></li>
<li><a href="#">Tutorials</a></li>
</ul>
</body>
</html>Try it Yourself »Do not nest too deep. Two or three levels is plenty. Deep nesting creates long, strong selectors that are hard to override, the same problem as long selectors written by hand.
Exercise
Exercise
Which selector styles a .card only when it contains a <video> element?
