⚡IntermediateAdvanced Selectors

CSS :is(), :not(), and :has()

Write concise, powerful selectors using the functional pseudo-classes :is(), :not(), :where(), and the CSS parent selector :has().

Overview

Modern CSS has three powerful functional pseudo-classes that change how you write selectors: - **:is()** matches any element from a list; reduces repetition - **:not()** excludes elements that match the argument - **:has()** the "parent selector"; matches an element that contains a matching descendant - **:where()** like :is() but with zero specificity Before :is(), writing "h1, h2, h3" variants required repeating the full selector path. Now you can write `.article :is(h1, h2, h3)` once. `:has()` is the most significant new CSS feature in years. `section:has(img)` selects any section that contains an image previously impossible in pure CSS.

Key Points

  • •:is(h1, h2, h3) matches any of h1, h2, or h3 eliminates repetitive selector lists
  • •:is() specificity equals the most specific selector in its argument list
  • •:not(.disabled) selects all elements that do NOT match .disabled
  • •:not() now accepts complex selectors: :not(.active, .selected)
  • •:has(img) selects any element containing an img the "parent selector"
  • •:has() can combine: a:has(> img) matches links with a direct child image
  • •:where() is identical to :is() but contributes zero specificity

Examples

:is() to style multiple heading levels at once

/* Without :is() tedious repetition */
.article h1, .article h2, .article h3 {
  font-weight: 700;
  color: #0f172a;
}

/* With :is() clean and DRY */
.article :is(h1, h2, h3) {
  font-weight: 700;
  color: #0f172a;
}

:is() specificity = highest specificity in the list. Here h1, h2, h3 all have (0,0,1) so the result is (0,0,1).

:not() to exclude elements from a rule

/* Style all buttons EXCEPT disabled ones */
.btn:not([disabled]):not(.loading) {
  cursor: pointer;
  transition: background 0.2s ease;
}

.btn:not([disabled]):not(.loading):hover {
  background: #4f46e5;
}

/* All list items except the last */
.menu li:not(:last-child) {
  border-bottom: 1px solid #e5e7eb;
}

:has() the parent selector

/* Card with an image gets different padding */
.card:has(img) {
  padding: 0;
  overflow: hidden;
}

/* Figure that has a figcaption gets extra bottom margin */
figure:has(figcaption) {
  margin-bottom: 32px;
}

/* Form row that has an invalid input gets highlighted */
.form-row:has(input:invalid) {
  background: #fff1f2;
  border-radius: 8px;
}

:has() is supported in all modern browsers as of 2023.

Common Mistakes

  • :is() takes its specificity from the highest-specificity selector in the list :is(#id, .class, p) has ID-level specificity even when matching p
  • Using :not() with complex selectors in older browsers :not() only accepted simple selectors before CSS Selectors Level 4
  • Expecting :has() to work in Firefox before version 121 (December 2023) use @supports to check

Note

Use :where() instead of :is() in design system utilities and reset styles to keep specificity at zero, making it easy for users to override your defaults without fighting specificity.

Exercises

Write CSS to style the HTML scaffold. The preview updates live — when it looks right, mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is the specificity of ":is(#id, .class, p)"?