CSS Selectors Level 4: :has, :is, :where, :not, :nth-child of
AI generated
CSS · Selectors Level 4 · Pseudo-Classes · Specificity
CSS Selectors Level 4
: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.

13 min read :has · :is · :where · :not · :nth-child of · relative selectors Chrome 105+ · Safari 15.4+ · Firefox 121+

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

11. FAQ: CSS Selectors Level 4

1What is :has() in CSS?
The parent selector. .card:has(img) matches cards with an image. label:has(+ input:invalid) matches labels with an invalid following input. The first genuine parent selector in CSS.
2:is() vs. :where()?
:is() inherits the highest specificity of its arguments. :where() always has specificity 0-0-0. For overridable base styles: :where(). For normal selectors: :is().
3:not() Level 4 vs. Level 3?
Level 4 allows selector lists: a:not([href], [aria-hidden]). Level 3 only single simple selectors. Specificity equals the most specific argument.
4What is :nth-child of?
:nth-child(2n of .item) only counts matching elements, not all siblings. Zebra striping and group selection work correctly in mixed DOMs.
5Why does :is(h1, #id) have ID specificity?
:is() inherits the specificity of the most specific argument. #id = 1-0-0, all elements matched through :is(h1, #id) get 1-0-0. For zero-specificity lists: :where().
6Browser support?
:has(), :is(), :where(), :not() Level 4: Chrome 105+, Safari 15.4+, Firefox 121+. Very good support for production use in 2026.
7Is :has() performant?
Yes: modern browsers have optimizations for common :has() patterns. Test with very large DOMs and complex selectors. Simple class arguments are unproblematic.
8Nest :has() inside :not()?
Yes. figure:not(:has(figcaption)) matches figure without figcaption. Very useful for context-sensitive styling without extra HTML classes.
9What are relative selectors?
Selectors that begin with a combinator: > .child, + .next. Valid inside :has(): .item:has(> .badge) matches .item with a direct .badge child. Cannot be used outside :has().
10:where() for design systems?
Yes: :where() for all base styles and reset rules that should be easily overridable. Any later rule, even h2 with 0-0-1, beats :where(h2) with 0-0-0.