learn.colinkim.dev

Modern selectors with :is(), :where(), :not(), and :has()

Learn modern selector helpers, relational selection, specificity behavior, and maintainable patterns.

Modern selectors make CSS more expressive without long repeated selector lists.

:is()

:is() groups selector options:

:is(h1, h2, h3) {
  line-height: 1.15;
}

It matches any listed selector. Its specificity is the specificity of the most specific selector in the list.

Useful for reducing repetition:

.article :is(h2, h3, h4) {
  margin-block-start: 2em;
}

:where()

:where() also groups selectors, but has zero specificity:

:where(h1, h2, h3) {
  margin-block: 0;
}

This is excellent for base styles that should be easy to override.

.prose :where(ul, ol) {
  padding-inline-start: 1.5em;
}

:not()

:not() excludes matches:

.nav-link:not([aria-current="page"]) {
  color: inherit;
}

Use it for simple exclusions. If a selector becomes a paragraph of exceptions, split the CSS into clearer classes or states.

:has()

:has() selects an element based on what it contains or relates to:

.field:has(input:required) label::after {
  content: " *";
}

It can also look at state:

.card:has(:focus-visible) {
  outline: 3px solid royalblue;
  outline-offset: 4px;
}

Or choose layout based on content:

.media-card:has(img) {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 1rem;
}

Specificity with modern selectors

:is(), :not(), and :has() use the specificity of the most specific selector in their argument list.

:where() always adds zero specificity.

That makes :where() useful in resets and base layers:

@layer base {
  :where(a) {
    color: var(--link);
  }
}

Components can override it easily:

@layer components {
  .button {
    color: white;
  }
}

Browser support strategy

For newer selectors, check MDN, Baseline, and caniuse for your target browsers. When a selector is an enhancement, wrap it:

@supports selector(.card:has(img)) {
  .card:has(img) {
    display: grid;
    grid-template-columns: 8rem 1fr;
  }
}

The base .card style still works if :has() is unavailable.

What to carry forward

  • :is() groups selectors with normal specificity behavior
  • :where() groups selectors with zero specificity
  • :not() excludes matches
  • :has() selects based on descendants, siblings, or state relationships
  • use @supports selector(...) for progressive enhancement when support matters
  • modern selectors help most when paired with clear HTML and low-specificity CSS

The next lesson covers native CSS nesting and scoped styling.

Progress

Quick checks

No quick checks in this lesson.

Mark lesson manually or answer quick checks to track progress.