CSS:not(), :is() & :where()

:not(), :is() & :where()

CSS selectors can get long and repetitive once a page grows past a handful of components. Three "functional" pseudo-classes — :not(), :is(), and :where() — let you write selector lists more compactly and, in the case of :not() and :is(), exclude or combine matches without duplicating whole selector chains. Each one accepts a selector (or a comma-separated list of selectors) as its argument.

:not() — exclude matches

:not(selector) matches any element that does NOT match the selector passed to it. It is the negation pseudo-class, and it can take a single selector or, in modern browsers, a comma-separated list.

CSS
/* Every button except the one with class .primary */
button:not(.primary) {
  background: #eee;
  color: #333;
}

/* Every list item that is not the last child */
li:not(:last-child) {
  border-bottom: 1px solid #ddd;
}

/* Modern :not() accepts a selector list */
input:not([type="checkbox"], [type="radio"]) {
  padding: 0.5em;
}
:is() — match any of several selectors

:is(selector-list) matches an element if it matches ANY selector in the list. It is most useful for collapsing long, repetitive selector groups into a single, readable rule.

CSS
/* Before: repetitive and hard to scan */
header nav ul li a,
header nav ol li a,
footer nav ul li a,
footer nav ol li a {
  text-decoration: none;
}

/* After: :is() collapses the repeated parts */
:is(header, footer) nav :is(ul, ol) li a {
  text-decoration: none;
}

Both rules above target the exact same elements. The :is() version is shorter, easier to maintain, and communicates intent — "either a header or a footer, containing either an ordered or unordered list."

:where() — same matching, zero specificity

:where() accepts the exact same syntax as :is() and matches exactly the same elements. The difference is entirely about specificity.

The key difference
:is() takes on the specificity of its MOST specific argument, just like a normal selector would. :where() ALWAYS has zero specificity, no matter what you put inside it. This makes :where() ideal for writing "easy to override" base styles.

CSS
/* :is() specificity = specificity of ".card" (0,1,0) */
:is(section, .card) p {
  color: #333;
}

/* :where() specificity = 0, regardless of contents */
:where(section, .card) p {
  color: #333;
}

/* A consumer of a design system can override the :where()
   version with a single-class selector — no fighting the
   library's specificity budget. */
.card p {
  color: firebrick;
}

Feature

:is()

:where()

Matches

Any selector in the list

Any selector in the list (identical)

Specificity

Specificity of its most specific argument

Always zero, regardless of arguments

Typical use

Shortening repetitive selectors

Writing override-friendly base/reset styles

Invalid selector in list

Whole rule is ignored (unless using the forgiving list)

Whole rule is ignored (unless using the forgiving list)

Note
Because :where() carries zero specificity, it is a favorite in CSS resets, component libraries, and utility frameworks: authors can ship sensible defaults using :where(), fully confident that anyone consuming the library can override them with an ordinary class selector instead of needing !important or ID selectors.
Combining them
  • Use :is() to shorten repetitive selector groups you write and control yourself.

  • Use :where() when writing library, reset, or "default" styles that consumers should be able to beat easily.

  • Use :not() to exclude specific matches from an otherwise broad selector.

  • All three accept selector lists, so they compose well with each other, e.g. :not(:is(.card, .panel)).