Using Modern Selectors
CSS has evolved significantly in recent years. With selectors like :is(), :where(), or :has(), tools have been added that are not just convenience features, but have a real impact on how we think about components and UI logic.
Many teams still barely use these selectors today, often out of habit or uncertainty. Yet they solve problems that regularly occur in large codebases: overly complex selectors, fragile components, excessive specificity, and unnecessary dependencies on JavaScript.
This door shows how modern selectors work, what problems they solve, and how to use them sensibly in real projects.
:is() – Simplifying Complex Selectors
The :is() selector allows you to group multiple variants into a single selector. This reduces repetition, lowers susceptibility to errors, and can improve readability.
An example from a navigation menu:
nav :is(a, button, [role="menuitem"]) {
padding: 0.5rem 1rem;
display: inline-flex;
align-items: center;
}
Previously, you would have written it like this:
nav a,
nav button,
nav [role="menuitem"] {
padding: 0.5rem 1rem;
display: inline-flex;
align-items: center;
}
The functionality is identical, but :is() reduces repetition and the risk of forgetting a selector.
Forgiving Selector List
An important advantage of :is() and :where(): They use a forgiving selector list. With traditional selector lists, the entire rule is discarded if one selector is invalid:
/* Completely ignored because :future-pseudo is invalid */
nav a,
nav :future-pseudo {
color: blue;
}
With :is(), invalid selectors are simply skipped:
/* Works for nav a, the invalid part is ignored */
nav :is(a, :future-pseudo) {
color: blue;
}
This is particularly valuable for progressive enhancement and future-proof code.
Note on Specificity
Important:
:is() takes on the highest specificity of the contained selectors.
Example:
:is(.btn, #cta) { ... }
Specificity: 1-0-0 (because of #cta)
In professional codebases, one should therefore use :is() consciously.
:where() – Selectors with Specificity 0-0-0
:where() works similarly to :is(), but sets the specificity of its content to 0-0-0. Selectors outside of :where() retain their normal specificity:
.container :where(.card .title) { ... }
/* Specificity: 0-1-0 (only .container counts) */
Example:
:where(.card .title) {
margin-bottom: 0.5rem;
}
No matter how complex the selector: Specificity remains 0-0-0.
This is particularly valuable for component base styles that should often be overridden without triggering a specificity cascade.
A real case:
/* Base Styling */
:where(.button) {
padding: 0.75rem;
background: var(--primary);
}
/* Variant */
.button.is-secondary {
background: var(--secondary);
}
Without :where(), .button would have specificity 0-1-0, making variants (0-1-1) more difficult.
With :where(), the base style remains intentionally “weak”.
In larger codebases, this can contribute massively to stability.
:has() – The First Real “Parent Selector” Feature
:has() is perhaps the most powerful new feature in CSS.
It allows styling based on the state or content of child elements. Something that was previously only possible with JavaScript.
Specificity with :has()
:has() behaves like :is() – it takes on the highest specificity of the contained selectors:
.card:has(#featured-image) { ... }
/* Specificity: 1-1-0 (ID + class) */
.card:has(.thumbnail) { ... }
/* Specificity: 0-2-0 (two classes) */
Example 1: Display card differently with image
.card:has(img) {
padding-top: 0;
}
Previously, you would have needed an extra class for this:
<div class="card card--with-image">...</div>
With :has(), CSS becomes more context-aware and components become more independent of markup.
Example 2: Validation states without JS
.form-group:has(input:invalid) {
border-color: var(--error);
}
This makes small interactions significantly easier.
Example 3: Open menu when toggle is active
With :has() on a shared container, you can control a menu without JavaScript:
<nav class="nav-container">
<input type="checkbox" id="menu-toggle" class="menu-toggle" hidden>
<label for="menu-toggle">Menu</label>
<ul class="menu">...</ul>
</nav>
.nav-container:has(.menu-toggle:checked) .menu {
display: block;
}
Previously, this would have been a case for JavaScript. Today, CSS is enough – provided the toggle and menu share a common ancestor.
Combined Examples from Real Projects
1. State-driven Styling for an Accordion Component
Before CSS :has(), you could only style sibling elements or children:
.accordion .header[aria-expanded="true"] + .content {
display: block;
}
With :has(), you can style the container itself based on the state of a child element:
.accordion:has([aria-expanded="true"]) {
background: var(--expanded-bg);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
This enables visual states at the container level without having to set additional classes via JavaScript.
2. Navigation with Active Links
Many systems use JavaScript or server-side logic:
nav a.active {
font-weight: 600;
}
But with :has():
nav li:has(a[aria-current="page"]) {
background: var(--highlight);
}
The list item – not just the link – can thus be highlighted.
3. Form Field with Error State
Previously:
<div class="field field--error">
<input>
</div>
With CSS:
.field:has(input:invalid) {
border-color: var(--error);
}
This enables logic close to the component, without additional classes.
When to Use Modern Selectors Consciously
Very Sensible:
- Unifying variants (
:is()) - Reducing specificity (
:where()) - Making components context-dependent (
:has()) - Interactive states without JS (
:has()+ pseudo-classes) - Simplifying complex components
Use Carefully:
- Deep selector structures with
:has() - Unintentionally increasing specificity with
:is() - Selecting very large DOM areas (consider performance)
In practice, modern selectors perform very well as long as they are not used for extremely broad selectors.
Browser Support
:is() and :where() have been supported by all modern browsers since 2021. :has() came later: Safari 15.4 (2022), Chrome 105 (2022), Firefox 121 (late 2023). For projects with legacy browser requirements, you should check support – especially for :has().
Conclusion
Modern selectors are one of the reasons why CSS today is more flexible and powerful than ever before. They reduce the need for additional classes, decrease JavaScript dependencies, and make components more robust and declarative.
Anyone who has :is(), :where(), and :has() as fixed tools in their repertoire builds modern, maintainable UI architectures – exactly as CSS is intended today.
The next doors build further on this foundation.
Comments