Articles

CSS :not() Selector Explained

The CSS :not() selector excludes elements matching an argument list from a rule, letting you style everything except specific cases without extra classes.

Takina Takina · · 4 min read
Close-up of CSS code on a screen

The :not() pseudo-class matches any element that does not match the selector (or selectors) passed to it. Instead of writing a rule and then overriding it for one exception, you write the exception directly into the selector: p:not(.intro) styles every paragraph except the ones with the intro class.

Basic syntax

:not() takes a selector as its argument and negates it:

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

/* Every button except disabled ones */
button:not(:disabled) {
  cursor: pointer;
}

/* Every input except checkboxes and radios */
input:not([type="checkbox"]):not([type="radio"]) {
  padding: 0.5rem;
}

Chaining :not() calls, as in the last example, works like a logical AND — the element must fail to match every negated selector to be styled.

The selector list argument

Modern CSS lets :not() accept a comma-separated list of selectors, matching an element that fails to match any of them. This replaces the older pattern of chaining separate :not() calls:

/* Modern: one call, a list of selectors */
.card:not(.featured, .archived) {
  opacity: 0.8;
}

/* Equivalent, older style */
.card:not(.featured):not(.archived) {
  opacity: 0.8;
}

Both produce the same result. The list form reads more like natural language — “not featured or archived” — and is generally preferred for new code.

Specificity: the trap

This is the part that catches people off guard. :not() itself contributes zero specificity, but the specificity of its argument is added to the selector as a whole. See what CSS specificity is for the full calculation rules, but the short version:

/* Specificity of a single class: (0,1,0) */
.card:not(.featured) { }

/* Specificity of an ID: (1,0,0) — much higher */
.card:not(#hero) { }

Swapping a class for an ID inside :not() can unexpectedly outrank other rules in your stylesheet. When a :not() rule isn’t applying the way you expect, check the specificity of what’s inside the parentheses, not just the selector outside it.

When a selector list is passed, the specificity used is that of the most specific selector in the list — even if the element only avoided the least specific one.

:not() vs :is() and :where()

:not(), :is(), and :where() all accept a selector list, but they answer different questions:

Pseudo-classMatches whenSpecificity
:not(list)element does not match any selector in the listspecificity of the most specific selector in the list
:is(list)element matches any selector in the listspecificity of the most specific selector in the list
:where(list)element matches any selector in the listalways zero

A common pattern combines them: use :where() to group a broad set of exceptions with zero specificity cost, then negate the whole group with :not():

.card:not(:where(.featured, .archived, .draft)) {
  opacity: 0.8;
}

This keeps the specificity of the rule pinned to .card alone, regardless of how many exceptions you add inside :where().

Practical patterns

Styling all but the first or last item, without :nth-child() math:

.nav-item:not(:first-child) {
  margin-left: 1rem;
}

Excluding a component’s own root from a descendant rule, useful when a component wraps another instance of itself:

.panel :not(.panel) > .title {
  font-weight: 600;
}

Combining with attribute selectors to target form fields that aren’t in an error or disabled state:

input:not([aria-invalid="true"], [disabled]) {
  border-color: var(--border-default);
}

Each of these avoids adding an extra class purely to opt an element out of a rule — the exception lives in the CSS instead of the markup.

When not to reach for it

:not() is a negation, not a general escape hatch. If a rule needs three or four negated selectors to express “everything except this specific handful of cases,” it’s often clearer — and easier to maintain — to give the exceptions a shared class and select on that directly. Negation reads well for one or two exclusions; past that, a positive selector for the group you actually want to style is usually less surprising to the next person reading the stylesheet.

It’s also worth remembering that :not() can only negate simple, compound, or (with the selector-list form) complex selectors — it can’t express “not inside an ancestor,” which needs a different structural approach, sometimes involving :has() on the ancestor instead.

The takeaway

:not() excludes elements matching its argument from a rule, and since browsers widely support the selector-list form, one call with a comma-separated list usually replaces a chain of separate :not() calls. The one thing to watch is specificity: the selector inside the parentheses contributes its own weight to the rule, so wrapping exceptions in :where() before negating them keeps a rule’s specificity predictable as the exception list grows.

Takina Takina · · 4 min read

Container Query Units: cqw, cqh, and cqi Explained

Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.

#CSS #Web Development #Frontend