9 min read

The :not(), :is(), :where(), and :has() CSS Selectors

9 min read
232 words
15 sections13 code blocks

Modern Functional Pseudo-Classes

CSS provides four powerful functional pseudo-classes that let you write more concise, flexible selectors. These selectors accept other selectors as arguments, enabling complex targeting with minimal code.

:not() — The Negation Selector

:not() targets elements that do NOT match the given selector:

CSS
/* All links that don't have a class */
a:not([class]) {
  text-decoration: underline;
  color: #3498db;
}

/* All inputs except submit buttons */
input:not([type="submit"]) {
  border: 1px solid #ddd;
  padding: 0.5rem;
}

/* All children except the last */
li:not(:last-child) {
  border-bottom: 1px solid #eee;
}

Multiple Negations

CSS
/* Elements that are not a heading and not a paragraph */
:not(h1):not(h2):not(h3):not(p) {
  font-size: 0.9rem;
}

/* Modern: multiple selectors in one :not() */
:not(h1, h2, h3, p) {
  font-size: 0.9rem;
}

Practical :not() Patterns

CSS
/* Style all form inputs except disabled ones */
input:not(:disabled) {
  cursor: pointer;
}

/* All buttons except primary */
.btn:not(.btn-primary) {
  background: white;
  color: #333;
}

/* Remove margin from last item without targeting it directly */
.card:not(:last-child) {
  margin-bottom: 1.5rem;
}

/* All images that don't have alt text */
img:not([alt]) {
  border: 3px solid red; /* Accessibility debugging */
}

:is() — The Matches-Any Selector

:is() matches any element that matches ANY of the selectors in its argument list. It reduces repetition in complex selectors:

CSS
/* WITHOUT :is() - repetitive */
header a:hover,
nav a:hover,
footer a:hover {
  color: #3498db;
}

/* WITH :is() - concise */
:is(header, nav, footer) a:hover {
  color: #3498db;
}

More :is() Examples

CSS
/* Style headings inside articles or sections */
:is(article, section, aside) :is(h1, h2, h3) {
  color: #2c3e50;
  line-height: 1.3;
}

/* Interactive states for multiple elements */
:is(.btn, .link, .tab):is(:hover, :focus) {
  opacity: 0.8;
}

/* All headings */
:is(h1, h2, h3, h4, h5, h6) {
  font-family: 'Poppins', sans-serif;
  line-height: 1.2;
}

:is() Specificity

Important: :is() takes the specificity of its most specific argument:

CSS
/* Specificity = (0,1,0,0) because #id is the most specific */
:is(.class, #id, element) {
  color: red;
}

:where() — Zero-Specificity :is()

:where() works exactly like :is() but has zero specificity. This makes it easy to override:

CSS
/* Specificity: (0,0,0,0) — very easy to override */
:where(header, nav, footer) a {
  color: #3498db;
}

/* This overrides the above easily */
a.special {
  color: red;
}

:where() for Default Styles

:where() is perfect for creating base styles that are easy to override:

CSS
/* Base styles with zero specificity */
:where(.card) {
  padding: 1.5rem;
  border-radius: 8px;
  background: white;
}

:where(.btn) {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

/* Component-specific overrides win easily */
.card {
  padding: 2rem; /* Overrides :where(.card) */
}

:is() vs :where() Comparison

CSS
/* :is() has the specificity of its most specific argument */
:is(.card) p { color: blue; }   /* Specificity: (0,0,1,1) */

/* :where() always has zero specificity */
:where(.card) p { color: blue; } /* Specificity: (0,0,0,1) */

:has() — The Parent Selector

:has() is the most revolutionary CSS selector. It lets you select a parent based on its children — something that was impossible in CSS before:

CSS
/* Select a card that contains an image */
.card:has(img) {
  grid-column: span 2;
}

/* Select a form that has an invalid input */
form:has(input:invalid) {
  border: 2px solid #e74c3c;
}

/* Select a label whose adjacent input is focused */
label:has(+ input:focus) {
  color: #3498db;
}

Practical :has() Examples

CSS
/* Style parent when child is hovered */
.nav-item:has(a:hover) {
  background: #f5f5f5;
}

/* Card layout changes when it has an image */
.card:has(img) {
  display: grid;
  grid-template-columns: 200px 1fr;
}

.card:not(:has(img)) {
  display: block;
}

/* Section with no content */
section:has(> :only-child) {
  padding: 2rem;
}

/* Style based on checkbox state */
.settings:has(input[type="checkbox"]:checked) {
  background: #e8f5e9;
}

:has() as a Conditional Selector

CSS
/* If the page has a sidebar, adjust main content width */
.page:has(.sidebar) .main-content {
  max-width: 70%;
}

.page:not(:has(.sidebar)) .main-content {
  max-width: 100%;
}

/* If a list has more than 5 items, use smaller font */
ul:has(li:nth-child(6)) {
  font-size: 0.9rem;
}

Combining Functional Pseudo-Classes

CSS
/* All links in header/footer that are not active */
:is(header, footer) a:not(.active) {
  opacity: 0.7;
}

/* Cards that have images but not videos */
.card:has(img):not(:has(video)) {
  aspect-ratio: 16/9;
}

/* Base styles easy to override */
:where(:is(h1, h2, h3)) {
  margin-bottom: 1rem;
}

Summary

Modern CSS functional pseudo-classes make selectors more powerful and concise. Use :not() for exclusion, :is() for grouping with normal specificity, :where() for grouping with zero specificity (ideal for defaults), and :has() for parent selection based on children. These selectors reduce code repetition, eliminate the need for extra CSS classes, and enable patterns that were previously impossible without JavaScript.