CSS course menu
Modern CSS Selectors: :is(), :where(), :has() and CSS Nesting
Learn modern CSS selectors: :is() to shorten selector lists, :where() for zero-specificity defaults, :has() the parent selector for styling based on children and siblings, and native CSS nesting with the & selector.
On this page
Newer CSS selectors make stylesheets shorter and do things that once needed JavaScript. All of the features in this lesson are supported in current versions of the major browsers.
:is(): shorter selector lists#
:is() matches any selector in its list, which removes repetition:
/* Without :is() */
article h2 a, article h3 a, aside h2 a, aside h3 a { color: inherit; }
/* With :is() */
:is(article, aside) :is(h2, h3) a { color: inherit; }The specificity of :is() is the specificity of its most specific argument.
:where(): zero specificity#
:where() works like :is(), but always has zero specificity. That makes it ideal for defaults that should be easy to override:
:where(.prose) a { color: crimson; } /* specificity (0,0,1): only the "a" counts */
a { color: green; } /* (0,0,1), and later, so it wins */<div class="prose"><p>A link: <a href="#">I am green</a></p></div>With .prose a instead, the class would make the first rule win. Libraries and resets use :where() so that your own simple selectors can always override them.
:has(): the parent selector#
:has() matches an element if it contains something that matches the selector inside. For the first time, CSS can style a parent based on its children:
.card { padding: 10px; border: 2px solid #ddd; border-radius: 8px; margin-bottom: 8px; }
.card:has(img) { border-color: #2f5fe0; }
.card:has(.badge-sale) { background: #fff4e6; }<div class="card">Text-only card</div>
<div class="card"><img src="/lesson-assets/mango.svg" alt="" width="30" height="30"> Card with an image (blue border)</div>
<div class="card"><span class="badge-sale">SALE</span> Card on sale (orange background)</div>Reacting to form state#
:has() can react to inputs inside an element, with no JavaScript:
.option { display: block; padding: 8px 12px; margin-bottom: 6px; border: 2px solid #ddd; border-radius: 8px; }
.option:has(input:checked) { border-color: #2f9e44; background: #ebfbee; }
form:has(input[type="checkbox"]:not(:checked)) button { opacity: 0.5; }<form>
<label class="option"><input type="radio" name="plan" checked> Free plan</label>
<label class="option"><input type="radio" name="plan"> Pro plan</label>
<label><input type="checkbox"> I accept the terms</label>
<p><button type="button">Continue</button></p>
</form>The selected option is highlighted, and the button looks faded until the terms box is ticked. (Fading is only visual; a real form must still prevent submission, for example with required on the checkbox.)
Looking at siblings#
Combined with + or ~, :has() can look forward at siblings:
/* A heading that is directly followed by a paragraph */
h2:has(+ p) { margin-bottom: 0.25rem; }CSS nesting#
CSS can now nest rules inside other rules, as preprocessors such as Sass have done for years:
.card {
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
h3 {
margin: 0 0 6px;
color: #1d3b4f;
}
&:hover {
border-color: #2f5fe0;
}
&.featured {
background: #eef3ff;
}
@media (min-width: 600px) {
padding: 20px;
}
}<div class="card featured">
<h3>Nested CSS</h3>
<p>Hover over this card.</p>
</div>- A nested selector without
&is a descendant:h3inside.cardmeans.card h3. &refers to the parent selector:&:hoveris.card:hover,&.featuredis.card.featured(no space).- Media queries can be nested inside a rule.
Keep nesting shallow, at one or two levels. Deep nesting produces long, highly specific selectors that are hard to override, the same problem described in Cascade, Specificity and Inheritance.
Practice#
Practice exercise
Using :has(), give a .field wrapper a red left border when its input is invalid and the user has interacted with it (:user-invalid), and a green one when its input is valid and not empty (:valid:not(:placeholder-shown)). Write it using nesting.
Show solution
.field {
padding: 6px 10px;
border-left: 4px solid #ddd;
margin-bottom: 8px;
&:has(input:user-invalid) { border-left-color: #c92a2a; }
&:has(input:valid:not(:placeholder-shown)) { border-left-color: #2f9e44; }
}<div class="field">
<label>Email <input type="email" placeholder="name@example.com" required></label>
</div>
<p>Type a valid address, or an invalid one and then click elsewhere.</p>Key takeaway#
Finished this lesson?