What is :not() in CSS?

:not() is a CSS pseudo-class that matches elements that do not match a selector you put inside the parentheses. In human words: “select everything like this… except that.”

  • It filters a selection. You usually combine it with another selector.
  • It accepts a selector list (comma-separated selectors) in modern CSS: :not(.a, .b).
  • It does not add extra specificity by itself. The specificity comes from what’s inside it.
.card:not(.featured) {
  opacity: 0.55;
}
  
.card:not(.featured) {
  opacity: 0.55;
}

.card.featured {
  outline: 4px solid #111;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  display: grid;
  gap: 12px;
  font-family: system-ui, Arial, sans-serif;
}

.grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.card {
  border: 3px solid #111;
  border-radius: 16px;
  background: #fff;
  padding: 14px;
  display: grid;
  gap: 8px;
}

.badge {
  justify-self: start;
  padding: 6px 10px;
  border: 2px solid #111;
  border-radius: 999px;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.02em;
}

.muted {
  opacity: 0.8;
  font-size: 14px;
}
  
Normal Card B

I’m getting filtered by :not().

Normal Card C

Me too.

The mental model

Think of :not() like a sieve: you start with some elements, and then you remove the ones that match the selector inside :not().

For example, .card:not(.featured) means: “select elements with class card, then remove the ones that also have featured.”

CSS :not() selector basics

Here are beginner-friendly patterns you’ll use all the time.

  • Exclude by class: .item:not(.disabled)
  • Exclude by attribute: a:not([href^="#"])
  • Exclude a state: button:not(:disabled)
a:not([href^="#"]) {
  text-decoration-thickness: 4px;
}
  
a:not([href^="#"]) {
  text-decoration-thickness: 4px;
}

a[href^="#"] {
  opacity: 0.6;
}
  
a:not([href^="#"]) {
  text-underline-offset: 6px;
  text-decoration-style: wavy;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 920px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.links {
  display: grid;
  gap: 10px;
}

a {
  color: #111;
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 2px;
}

.note {
  font-size: 14px;
  opacity: 0.8;
}
  

Goal: style “real links” differently from page anchors.

Section 2: hi 👋

Learn more about attribute selectors in the CSS Attribute Selector Interactive Tutorial.

CSS :not() with classes

The most common use: style everything except a class.

Chaining :not() vs a selector list

These two are essentially the same:

  • .item:not(.a):not(.b) means “not .a and not .b” (exclude both).
  • .item:not(.a, .b) means “not .a or .b” (also excludes both).

In practice, both exclude .a and .b. The difference is mostly readability and browser support: modern browsers handle the comma list fine, but if you need to support older browsers, chaining :not() is safer.

.pill:not(.primary) {
  opacity: 0.5;
}
  
.pill:not(.primary, .danger) {
  opacity: 0.5;
}
  
.pill:not(.primary):not(.danger) {
  opacity: 0.5;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.pill {
  border: 2px solid #111;
  border-radius: 999px;
  padding: 8px 12px;
  font-weight: 800;
  letter-spacing: 0.02em;
  background: #fff;
}

.primary {
  background: #111;
  color: #fff;
}

.danger {
  background: #ffefef;
}
  

Try each snippet. Notice how the “excluded” pills change depending on what you put inside :not().

primary danger neutral neutral

CSS :not(:first-child)

A classic layout move: apply spacing to items except the first one. This avoids awkward “extra space at the top”.

Spacing list items without touching the first

.item:not(:first-child) is usually nicer than manually adding a class to “all but the first”.

.item:not(:first-child) {
  margin-top: 12px;
}
  
.item:not(:first-child) {
  margin-top: 12px;
}

.item:first-child {
  background: #111;
  color: #fff;
}
  
.item:not(:first-child) {
  border-top: 2px dashed #111;
  margin-top: 12px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 720px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.list {
  border: 3px solid #111;
  border-radius: 16px;
  background: #fff;
  padding: 14px;
}

.item {
  padding: 10px 12px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f6f6f6;
}
  

Goal: add separation between items, without pushing the whole list down.

First item (no spacing above)
Second item
Third item
Fourth item

Quick note: :first-child vs :first-of-type

:first-child means “literally the first child node element.” If your first item is preceded by another element (like a heading), your selector won’t match like you expect.

In those cases, you might want .item:not(:first-of-type) instead, or select a tighter parent like .list > .item:not(:first-child).

Learn more about :first-child and :first-of-type in the CSS First Child Interactive Tutorial and the CSS Nth Of Type Interactive Tutorial.

CSS :not(:last-child)

This is the sibling of the previous trick: apply styling to everything except the last one. Perfect for dividers.

.item:not(:last-child) {
  position: relative;
  padding-bottom: 14px;
  margin-bottom: 14px;
}

.item:not(:last-child)::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -10px;
  height: 2px;
  background: #111;
  opacity: 1;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 760px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.list {
  border: 3px solid #111;
  border-radius: 16px;
  background: #fff;
  padding: 14px;
}

.item {
  padding: 10px 12px;
  border-radius: 12px;
  background: #f6f6f6;
  border: 2px solid #111;
  display: grid;
  gap: 6px;
}

.item strong {
  font-size: 16px;
}

.item p {
  margin: 0;
  opacity: 0.8;
  font-size: 14px;
}
  

Goal: add separators between items, but don’t draw a divider after the last one.

Alpha

Divider below me.

Beta

Divider below me.

Gamma

No divider below me (I’m last).

Learn more about :last-child in the CSS Last Child Interactive Tutorial.

CSS :not(:hover)

You can use :not(:hover) to style elements only when they are not hovered. That sounds weird… until you try building “dim the others” effects.

Dim siblings when one is hovered

This pattern reads like: “when the container is hovered, dim every card that is not hovered.”

.grid:hover .card:not(:hover) {
  opacity: 0.35;
  transform: scale(0.98);
}
  
.grid:hover .card:not(:hover) {
  opacity: 0.35;
  transform: scale(0.98);
}

.card:hover {
  transform: scale(1.02);
}
  
.grid:hover .card:not(:hover) {
  opacity: 0.35;
  filter: blur(0.6px);
}

.card:hover .tag {
  background: #111;
  color: #fff;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.card {
  border: 3px solid #111;
  border-radius: 18px;
  background: #fff;
  padding: 14px;
  display: grid;
  gap: 10px;
  transition: opacity 180ms ease, transform 180ms ease, filter 180ms ease;
}

.tag {
  justify-self: start;
  border: 2px solid #111;
  border-radius: 999px;
  padding: 6px 10px;
  font-weight: 800;
  font-size: 12px;
  transition: background 180ms ease, color 180ms ease;
}

.title {
  font-weight: 900;
}

.muted {
  margin: 0;
  font-size: 14px;
  opacity: 0.8;
}
  

Hover one card. The rest get the “not hovered” styling.

Card
One

Hover me.

Card
Two

Or me.

Card
Three

Or me.

Learn more about :hover in the CSS :hover Pseudo-Class Interactive Tutorial.

CSS :not(:empty)

:empty matches elements with no child elements and no text. And here’s the sneaky part: whitespace counts as text.

So if you have <div class="msg"> </div> with a line break or spaces inside, it might not be considered empty anymore.

Show a UI only when it actually has content

A common pattern: hide all message boxes by default, but show them when they aren’t empty.

Switch to the HTML view, you will see the empty message box is there.

.msg {
  display: none;
}

.msg:not(:empty) {
display: block;
} 
.msg {
  display: none;
}

.msg:not(:empty) {
  display: block;
  border-left: 10px solid #111;
}
  
.msg {
  display: none;
}

.msg:not(:empty) {
  display: grid;
  gap: 6px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 920px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.panel {
  border: 3px solid #111;
  border-radius: 18px;
  background: #fff;
  padding: 14px;
  display: grid;
  gap: 12px;
}

.msg {
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  padding: 12px;
  font-weight: 700;
}

.msg strong {
  font-weight: 900;
}

.note {
  margin: 0;
  font-size: 14px;
  opacity: 0.85;
}
  

The first message has text. The second is truly empty.

Heads up: This box is not empty, so it shows.

The whitespace “gotcha”

If you write this:

  1. <div class="msg"> </div>
  2. <div class="msg">\n</div>

That whitespace can make the element not empty in many real HTML templates. If you depend on :empty, keep the element truly empty (no spaces, no line breaks).

CSS :not() with :has()

Now for the fun one: :has() lets you select an element based on what it contains. When you combine it with :not(), you can express things like: “Select cards that do not have a badge.”

Select elements missing something inside

This is super practical in UI work: style items differently when a sub-element is missing.

.card:not(:has(.badge)) {
  border-style: dashed;
}
  
.card:not(:has(.badge)) {
  border-style: dashed;
}

.card:has(.badge) .title::after {
  content: " ✓";
}
  
.card:not(:has(.badge)) .meta {
  opacity: 1;
  font-weight: 800;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  max-width: 980px;
  padding: 18px;
  font-family: system-ui, Arial, sans-serif;
  display: grid;
  gap: 12px;
}

.grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.card {
  border: 3px solid #111;
  border-radius: 18px;
  background: #fff;
  padding: 14px;
  display: grid;
  gap: 10px;
}

.badge {
  justify-self: start;
  border: 2px solid #111;
  border-radius: 999px;
  padding: 6px 10px;
  font-weight: 900;
  font-size: 12px;
}

.title {
  font-weight: 900;
}

.meta {
  margin: 0;
  font-size: 14px;
  opacity: 0.75;
}
  

Goal: visually flag cards that are missing a badge.

Badge
Card A

Has a badge.

Card B

No badge inside me.

Badge
Card C

Has a badge.

A real-world pattern: “invalid unless it has X”

You can use this to style “incomplete” blocks: .field:not(:has(input:valid)) or .section:not(:has(.is-ready)).

Just remember: :has() is powerful, but it’s also more expensive for browsers to evaluate than simple selectors. Use it intentionally, not everywhere.

Learn more about :has() in the CSS :has Pseudo-Class Interactive Tutorial.

Common :not() mistakes (and how to avoid them)

  • Mistake: using :empty but your template outputs whitespace.
    Fix: ensure the element is truly empty, or use a class like .is-empty / .has-content.
  • Mistake: writing :not(.a .b) and expecting it to mean “not .a and not .b” (forgetting the comma).
    Fix: use :not(.a):not(.b) or :not(.a, .b).
  • Mistake: forgetting that the selector inside :not() is relative to the element being matched.
    Fix: read it as “this element does not match X” (not “this element does not contain X” unless you use :has()).

Practical :not() recipes

Style buttons that are not disabled

  • button:not(:disabled)

Style nav links that are not active

  • .nav a:not(.is-active)

Spacing between items without affecting edges

  • .item:not(:first-child) for top spacing
  • .item:not(:last-child) for dividers

Dim everything except what the user is hovering

  • .grid:hover .card:not(:hover)

Highlight cards missing an element

  • .card:not(:has(.badge))

Conclusion

If you remember only one thing: :not() is a filter. It doesn’t “do magic” by itself—it just says “match this… except that.”

Once you get comfortable with :not(:first-child), :not(:last-child), :not(:hover), :not(:empty), and the spicy combo :not(:has(...)), you’ll start writing cleaner CSS with fewer helper classes.