:has, :is, :where, :not and relative selectors
Selectors Level 4 has fundamentally expanded the CSS toolbox. CSS Selectors Level 4 brings :has as a long-awaited parent selector, :is and :where for compact selector lists, an extended :not and a new :nth-child of syntax. These selectors replace many JavaScript workarounds and significantly reduce CSS redundancy.
Table of Contents
- 1. Why new selectors change so much
- 2. :has: the parent selector has arrived
- 3. :is: compact selector lists
- 4. :where: selection without specificity
- 5. :not: extended exclusion
- 6. :nth-child of: filtered child nodes
- 7. Relative selectors and :scope
- 8. Comparing selector specificity
- 9. CSS Selectors Level 4 in practice
- 10. Summary
- 11. FAQ
1. Why new selectors change so much
CSS selectors were an area that barely changed for years. The basics, such as type selectors, class selectors, ID selectors and combinator selectors, date back to the early days of the web. Anyone with more complex requirements reached for JavaScript, increased specificity through longer selectors, or added extra classes to the HTML. CSS Selectors Level 4 changes that fundamentally: with :has, :is, :where and the extended :not, selectors become far more expressive without having to add HTML classes. This significantly reduces coupling between HTML structure and styling.
The impact on daily work is noticeable: a product grid that should only show a certain layout when it contains products with badges used to be a JavaScript task. With CSS Selectors Level 4, .grid:has(.badge) solves this declaratively. Forms that should style themselves based on their content; navigation that changes its appearance when a specific child is active: all of this becomes a pure CSS task with the new selectors.
2. :has: the parent selector has arrived
The pseudo-class :has() is the most long-awaited selector in CSS history. It allows you to select an element based on its descendants or successors. figure:has(figcaption) matches all <figure> elements that contain a <figcaption>. This works as a genuine parent selector, but also as a "previous sibling" selector: label:has(+ input:required) matches a <label> immediately followed by a required input. CSS has never offered this capability before.
The specificity of :has() is based on the most specific argument in the parenthesized list. :has(.card) has the specificity of a class (0-1-0). :has(h2.title) has the specificity of h2 plus a class (0-1-1). This is intuitive and consistent with the behavior of :is(). In practice: :has() is not a performance problem in modern browsers, the engines have special optimizations for common :has() patterns. In very large DOMs with complex :has() selectors, however, testing is recommended.
/* :has: parent selector and sibling context selector */
/* Card with an image gets different padding */
.card:has(img) {
padding-block-start: 0;
}
/* Form field label turns red when sibling input is invalid */
label:has(+ input:invalid) {
color: #dc2626;
font-weight: 600;
}
/* Navigation item is highlighted when a child link is active */
.nav-item:has(> a[aria-current="page"]) {
background: #ede9fe;
border-inline-start: 3px solid #7c3aed;
}
/* Grid switches to single column when it has many items */
.product-grid:has(.product-card:nth-child(n + 7)) {
grid-template-columns: repeat(2, 1fr);
}
/* Figure without figcaption: no bottom margin */
figure:not(:has(figcaption)) {
margin-block-end: 0;
}
/* Section with a visible heading gets extra top spacing */
.content-section:has(h2:not([hidden])) {
padding-block-start: 3rem;
}
3. :is: compact selector lists
The pseudo-class :is() takes a selector list as an argument and matches elements that satisfy at least one of the selectors in the list. Its primary use case is simplifying long, redundant selectors. Instead of header h1, header h2, header h3, main h1, main h2, main h3 you write :is(header, main) :is(h1, h2, h3). This is not only shorter, but also easier to maintain: additions only require a change in one place in the code.
The specificity of :is() is the specificity of its most specific argument, not the average or minimum specificity. :is(h1, .title, #headline) always has the specificity of an ID (1-0-0), regardless of which element is actually matched. This can be surprising: an h1 matched through :is(h1, #main-title) inherits the ID specificity 1-0-0. Anyone who needs lower specificity should use :where(). This specificity rule fundamentally distinguishes :is() from :where().
/* :is: compact selector lists with inherited specificity */
/* Without :is: 6 separate selectors */
header h1, header h2, header h3,
footer h1, footer h2, footer h3 {
font-family: var(--font-display);
}
/* With :is: 1 selector, same effect */
:is(header, footer) :is(h1, h2, h3) {
font-family: var(--font-display);
}
/* Error states for multiple form elements */
:is(input, select, textarea):invalid {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
}
/* Focus-visible on all interactive elements */
:is(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid #7c3aed;
outline-offset: 2px;
border-radius: 0.25rem;
}
/* Heading margins reset in common containers */
:is(article, section, aside) > :is(h1, h2, h3, h4) {
margin-block-start: 0;
}
/* Specificity note: :is(h1, #id) has specificity 1-0-0 (the highest argument) */
/* Use :where instead if you need zero specificity */
4. :where: selection without specificity
:where() is structurally identical to :is(), it takes a selector list and matches elements that satisfy at least one selector. The crucial difference: :where() always has zero specificity (0-0-0), no matter how specific the selectors it contains are. That makes :where() the ideal tool for CSS reset stylesheets, base styles and design system foundations that should be easy to override.
In practice, :where() is used wherever you want to define styles that authors can override without a specificity escalation. A typography reset: :where(h1, h2, h3, h4, h5, h6) { font-weight: bold; margin: 0; }, any later rule, even h2 { font-weight: normal; } with specificity 0-0-1, overrides it. :where() and :is() together therefore give you complete control over specificity in selector lists: :is() inherits the highest specificity, :where() always has zero.
5. :not: extended exclusion
The :not() in CSS Selectors Level 4 is a substantial extension over the Level 3 version. Level 3 only allowed a single simple selector as an argument, no descendant selector, no selector list. Level 4 allows a full selector list with multiple, potentially complex selectors. :not(.error, .warning, .info) is now valid. a:not([href], [aria-hidden="true"]) matches links without an href and without aria-hidden="true". This simplifies many exception rules that used to be solved through counter-selection or increased specificity.
The specificity of :not() follows the same rules as :is(): it corresponds to the specificity of its most specific argument. :not(.active) has specificity 0-1-0. :not(#hero) has specificity 1-0-0. For zero-specificity exclusions you can combine :not(:where(.active)), which is valid and results in zero specificity for the exclusion. Important: :not() cannot be nested inside itself, :not(:not(.active)) is invalid.
/* :not Level 4: selector list arguments and complex selectors */
/* All links except external and anchor links */
a:not([href^="http"], [href^="#"], [href^="mailto:"]) {
color: #4a1d96;
text-decoration-color: rgba(124, 58, 237, 0.4);
}
/* All list items except the last one: no trailing divider */
li:not(:last-child) {
border-block-end: 1px solid #e2e8f0;
}
/* Inputs that are neither disabled nor read-only */
input:not(:disabled, [readonly]) {
cursor: text;
}
input:not(:disabled, [readonly]):focus {
border-color: #7c3aed;
}
/* Table rows: skip the header row */
tr:not(:has(th)) {
transition: background 0.15s ease;
}
tr:not(:has(th)):hover {
background: #faf5ff;
}
/* Images that are decorative need no alt: hide from assistive tech */
img:not([alt]) {
outline: 3px solid #dc2626; /* Dev warning: missing alt attribute */
}
6. :nth-child of: filtered child nodes
The extended :nth-child of syntax from CSS Selectors Level 4 solves a long-standing problem: the classic :nth-child(2n) counts all child nodes regardless of type. If a list mixes <li> and <div> elements, li:nth-child(2n) does not produce the intuitively expected result, it counts all child nodes, not just the <li> elements. The new syntax :nth-child(2n of li) counts only child nodes that match the given selector.
This enables zebra striping that is robust against mixed child nodes, selected rows in tables with group rows, and grid layouts that only capture certain element types. The syntax is :nth-child(An+B of S), where S is a selector list. :nth-child of also applies to :nth-last-child. :nth-child(1 of .featured) matches the first element with the class .featured among its siblings, regardless of any other sibling nodes.
7. Relative selectors and :scope
Relative selectors are another feature of CSS Selectors Level 4 that gains particular significance in combination with the JavaScript API element.querySelectorAll() and CSS @scope. A relative selector begins with a combinator: > .child, + .next, ~ .sibling. In regular CSS such selectors are invalid, selectors must begin with an element or a pseudo-class. Inside :has() and in querySelector, however, relative selectors are valid.
The :scope pseudo-class from Selectors Level 4 creates a reference to the context node. In querySelector this is the calling element. In @scope blocks it is the scope root. Outside both contexts, :scope behaves like :root and is therefore of little use in a global stylesheet context. The combination of relative selectors, :scope and :has() makes CSS Selectors Level 4 a fully expressive system for context-based styling.
8. Comparing selector specificity
The new pseudo-classes from CSS Selectors Level 4 behave differently when it comes to specificity calculation. :is() and :not() inherit the highest specificity from their argument list. :where() always has zero specificity. :has() inherits the specificity of its most specific argument, just like :is(). These rules are consistent, but can be surprising when using selector lists with mixed specificity levels.
| Selector | Example | Specificity | Level |
|---|---|---|---|
| :is() | :is(h1, .title) |
0-1-0 (class) | Level 4 |
| :where() | :where(h1, #id) |
0-0-0 (always zero) | Level 4 |
| :not() | :not(.active, #hero) |
1-0-0 (ID) | Level 4 |
| :has() | :has(.card) |
0-1-0 (class) | Level 4 |
| :nth-child of | :nth-child(2n of .item) |
0-1-0 (class) | Level 4 |
The most important practical consequence: when you use :is() for selectors with mixed specificities, e.g. :is(h2, .heading, #main-title), all matched elements get the specificity 1-0-0, even a plain h2. To avoid this, group selectors of the same specificity level within :is() and split different levels into separate rules, or use :where() for the non-specific ones.
9. CSS Selectors Level 4 in practice
In real projects, the benefit of CSS Selectors Level 4 shows up especially in form styling, navigation and content-based layouts. Forms often have complex states: an input is invalid, required, focused, disabled. With :is(input, select, textarea):not(:disabled):invalid this can be expressed compactly. Navigations show active states on various levels: .nav-item:has([aria-current]) captures this without having to duplicate the HTML state.
For Magento/Hyva projects these selectors are especially valuable in product listings and checkout flows. Product cards that have a sale badge get highlighted border styling via .product-card:has(.badge--sale). Checkout steps show progress via .step:has(+ .step--active). Tabular product comparisons switch layout with .compare-table:has(.compare-cell:nth-child(n+4)). CSS Selectors Level 4 turns such context-sensitive styles into pure CSS work.
/* CSS Selectors Level 4: practical combinations */
/* Form validation styles using :has and :is */
.form-group:has(input:invalid:not(:placeholder-shown)) .error-msg {
display: block;
color: #dc2626;
}
.form-group:has(input:valid:not(:placeholder-shown)) .success-icon {
display: inline-flex;
color: #16a34a;
}
/* Navigation: active section highlighting with :has */
.sidebar-nav .section:has(.nav-link[aria-current="page"]) {
background: #faf5ff;
border-inline-start: 3px solid #7c3aed;
}
/* :where for low-specificity base typography */
:where(article, .content, .prose) :where(h2, h3, h4) {
font-weight: 700;
line-height: 1.25;
color: #1e1b4b;
}
/* :nth-child of: zebra striping for mixed DOM */
.data-row:nth-child(odd of .data-row) {
background: #faf5ff;
}
/* :not with selector list: all links except nav links */
a:not(:is(.nav-link, .breadcrumb-link, .footer-link)) {
text-decoration: underline;
text-decoration-color: rgba(124, 58, 237, 0.5);
}
10. Summary
CSS Selectors Level 4 has fundamentally expanded the expressiveness of CSS selectors. :has() is the first genuine parent selector, enabling styling based on child nodes and successors. :is() compactly combines selector lists and inherits the highest specificity of its arguments. :where() does the same with zero specificity, ideal for overridable base styles. The extended :not() accepts selector lists and complex selectors. :nth-child of filters child nodes by type before counting.
Browser support in 2026 is very good: :has(), :is(), :where() and extended :not() run in Chrome 105+, Safari 15.4+ and Firefox 121+. :nth-child of has somewhat more limited support but is usable in all modern browsers. The most important rule for productive use: understand the specificity behavior of :is(), :not() and :has() to avoid unexpected cascade conflicts. :where() is the safe default choice for system styles that should be easy to override.
CSS Selectors Level 4: the essentials at a glance
:has()
Parent selector and ancestor selector. .card:has(img) matches cards with an image. label:has(+ input:invalid) matches a label with an invalid sibling input.
:is() vs. :where()
:is() inherits the highest specificity of its arguments. :where() always has 0-0-0. For base styles and overridable rules: use :where().
:not() Level 4
Accepts selector lists: a:not([href], [aria-hidden]). Specificity equals the most specific argument. Combine with :where() for zero-specificity exclusions.
:nth-child of
:nth-child(2n of .item) only counts elements that match .item. Robust for mixed DOM structures, zebra striping works correctly.
Mironsoft
Modern CSS, Hyva themes and Magento frontend development
CSS selectors that grow with your project?
We modernize CSS architectures in Hyva and Magento projects: from redundant selectors to compact, maintainable rules with :has, :is, :where and the full Selectors Level 4 arsenal.
Selector Audit
Analysis of redundant selectors and modernization with CSS Level 4: less code, more expressiveness
Form Styling
Implement complex form states cleanly with :has, :is and :not, without JavaScript class toggling
Design System
Base styles with :where and overridable component styles with :is for scalable systems