Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~26 min· topic 5 of 6

Topic 1.5

:not, :is, :where and :has

In one line

Four functional pseudo-classes add logic to selectors: :not() excludes, :is() and :where() say "any of these" (with full or zero specificity), and :has() selects an element by what it contains or what follows it, the long-awaited parent selector.

Think of it like this

Choosing players for a game. "Everyone except the goalkeeper" is :not(). "Anyone from class 5A or 5B" is :is(). "Anyone from 5A or 5B, but it's only a suggestion that any other rule can overrule" is :where(). "Every team that has a captain" is :has(): you pick the team by looking at who is in it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Functional pseudo-class
A pseudo-class with brackets that takes selectors inside it, like :not(.done).
Argument
What you put inside the brackets of a function, like .done in :not(.done).
Forgiving selector list
A list where a broken selector is skipped instead of breaking the whole rule. :is() and :where() are forgiving.
Specificity
The score that decides which of two clashing rules wins. Higher score wins.
Zero specificity
A score of nothing: any other rule that matches the same element will beat it.
Relative selector
A selector inside :has() that is read starting from the element being tested, like > img or + p.
Parent selector
A way to pick an element because of what is inside it. :has() is CSS's parent selector.

Step by step

01Exclude with :not()

Instead of styling everything and then undoing it for one element, say what you don't want.

Since 2021 every browser accepts a list inside :not(), so :not(.a, .b) means "neither a nor b".

style.csswhole filecss
.nav a:not(.active) {
  opacity: 0.7;
}
li:not(:last-child) {
  border-bottom: 1px solid #ddd;
}

02Shorten lists with :is()

Without :is(), styling headings inside three containers needs nine selectors. With it, one line.

style.csswhole filecss
/* before */
article h2, article h3, aside h2, aside h3, section h2, section h3 { line-height: 1.2; }

/* after */
:is(article, aside, section) :is(h2, h3) { line-height: 1.2; }

03Write overridable defaults with :where()

A reset written with :where() has zero specificity, so the plainest rule you write later still wins. Libraries use this to stay out of your way.

style.csswhole filecss
:where(ul[class], ol[class]) {
  list-style: none;
  padding: 0;
}
/* any later rule wins, even a bare type selector */
ul { padding-left: 20px; }

04Compare specificity

:is() takes the score of its strongest argument; :where() takes none. This one difference is the whole reason both exist.

Compare specificitydiagram
Rendering diagram…

05Select by content with :has()

Style a card differently when it contains an image, a form when a field is invalid, or a label when its checkbox is ticked. Before :has() each of these needed JavaScript to add a class.

style.csswhole filecss
.card:has(img) {
  display: grid;
  grid-template-columns: 80px 1fr;
}
form:has(input:invalid) button {
  opacity: 0.5;
}
label:has(input:checked) {
  font-weight: 700;
}

06Look forward and backward with :has()

Relative selectors inside :has() can start with a combinator. h2:has(+ p) selects a heading by the element after it, the "previous sibling" selector CSS never had.

style.csswhole filecss
h2:has(+ p) {
  margin-bottom: 4px;
}
li:has(+ li.active) {
  color: #6b7280;
}

Try it yourself

  1. 1

    Swap :is for :where

    In the second example, change :is(#featured, .card) h2 to :where(#featured, .card) h2. Predict both card colours, then run: both turn seagreen, because the :where() rule now scores zero.

  2. 2

    Add a picture to the second card

    In the third example, copy the <svg>…</svg> into the second card. Predict the log line for cards with a picture (2), then run.

  3. 3

    Exclude one more type

    In the first example, add <input type="radio" name="r"> to the form. Predict whether it gets the box style (no) and what the third log line prints (still 2).

Code & diagrams

Excluding with :not() New tab

Each rule says what to leave out, so you never have to undo styles with a second rule.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

dimmed links: 2
items with a divider: 2
boxed inputs: 2
:is() versus :where(): same matching, different specificity New tab

Card B has no id, yet it is crimson: the `:is()` rule scores as an ID selector for every element it matches. The `:where()` rule loses even to a bare `li`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Card A: rgb(220, 20, 60)
Card B: rgb(220, 20, 60)
List item: rgb(46, 139, 87)
:has(): the parent and previous-sibling selector Baseline 2023+ New tab

Tick or untick a box: the label restyles with no JavaScript, because `:has(input:checked)` is re-checked as the state changes.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

cards with a picture: 1
ticked options: 1
headings before a paragraph: 1

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

:is() makes a rule unexpectedly hard to override

Write :is(#sidebar, .panel) a { color: crimson; } and later .panel .link { color: seagreen; } for links inside .panel.

terminal
$ the preview shows
── what you'll see ──
Links inside .panel stay crimson, even though the seagreen rule comes later and is more specific-looking.

Break #2

An unsupported selector inside :has() kills the rule

Write .card:has(img, :hovr) { border-color: blue; }.

terminal
$ the preview shows
── what you'll see ──
No card gets the blue border, not even the ones with an image.

Myth vs fact

Myth

:where() is just a newer name for :is().

Fact

They match the same elements, but :where() always has zero specificity and :is() takes its strongest argument's. Pick based on whether the rule should be easy to override.

Myth

:has() is too slow to use.

Fact

Browsers optimised :has() heavily before shipping it. Keep the argument specific (.card:has(> img) rather than :has(*) on body) and it is fine for normal pages.

Myth

:not(.a .b) is not allowed.

Fact

Since Selectors Level 4 (shipped in every browser by 2021), :not() accepts complex selectors and lists, such as :not(.a .b) and :not(.x, .y).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Specificity of :not(), :is() and :has() is the specificity of the most specific selector in their argument, computed once for the rule, not per matched element. That's why :is(#id, .class) acts as an ID even for elements matched by .class.

  • ▸

    :has() is the first selector whose result depends on later and deeper elements, so the browser must re-check ancestors and earlier siblings when content changes. Engines track which elements could be affected; an anchored argument (:has(> img), :has(+ p)) limits that work.

  • ▸

    :has() can't contain pseudo-elements or another :has(). Test support with @supports selector(:has(a)) if you must serve very old browsers, and keep the page usable without it.

  • ▸

    Forgiving lists make progressive enhancement safer: :is(:focus-visible, .js-focus) keeps working in an older browser that doesn't know :focus-visible, while the same pair in a plain list would drop the whole rule there. Pseudo-elements are not allowed inside :is(), :where() or :has().

Remember this

  1. 1

    A functional pseudo-class is a pseudo-class with brackets that takes other selectors as its argument, like :not(.active). The argument is a selector list: one or more selectors separated by commas.

  2. 2

    :not(X) matches elements that do not match X. li:not(:last-child) is every item except the last, input:not([type="checkbox"], [type="radio"]) is every input except those two types. Careful: :not(.a) on its own (no type in front) means "any element without class a", which includes html and body.

  3. 3

    :is(X, Y, Z) matches an element that matches any of the selectors in the list. It shortens repetitive selectors: :is(article, aside, section) h2 replaces three separate selectors. Its list is forgiving: an invalid or unsupported selector inside it is ignored instead of killing the whole rule.

  4. 4

    :where(X, Y) matches exactly like :is(), with one difference: its specificity is always zero. That makes it ideal for defaults (resets, library styles) that any later class should be able to override without a fight.

  5. 5

    Specificity (the priority score from Topic 1.1) of these four: :is(), :not() and :has() take the specificity of their most specific argument, so :is(#nav, .menu) a counts as an ID selector even when the element matched through .menu. :where() always counts zero. Phase 2 shows the full scoring.

  6. 6

    :has(X) matches an element if X, read relative to it, finds something. .card:has(img) is a card that contains an image; .card:has(> img) one whose direct child is an image; h2:has(+ p) an h2 followed immediately by a paragraph; form:has(input:invalid) a form with an invalid field. For the first time CSS can style a parent or an earlier sibling based on what comes inside or after it. It is Baseline 2023, supported in every current browser. Unlike :is(), its list is not forgiving, and :has() cannot be nested inside another :has().

Explain it without notes

01

Compare :is() and :where(): how do they match, and how does their specificity differ?

02

What is the specificity of .btn:not(#primary)? Why can that cause trouble?

03

What problem does :has() solve, and give three practical examples.

04

What is a forgiving selector list, and which of these four functions has one?

Practice

01

Give every button except those with class ghost a solid blue background, in one rule.

02

Write a reset that removes list bullets and padding from any ul or ol that has a class, with zero specificity so any later rule can add them back.

03

Highlight a .field wrapper with a red left border when the input inside it is invalid.

Trade-offs

  • ↔

    :is() shortens CSS but silently raises specificity to its strongest argument; :where() keeps specificity flat but can be overridden by accident.

  • ↔

    :has() removes a lot of class-toggling JavaScript, but its styles depend on page structure; very broad :has() rules on large, frequently changing pages can cost style recalculation time.

Done when you can

  • I can exclude elements with :not(), including lists.

  • I can shorten selectors with :is() and know it is forgiving.

  • I can write zero-specificity defaults with :where().

  • I can state the specificity of :not(), :is(), :where() and :has().

  • I can select parents and earlier siblings with :has().