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;
}
Featured Card A I’m the chosen one.
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.aand not.b” (exclude both). -
.item:not(.a, .b)means “not.aor.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 itemThird itemFourth 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.
AlphaDivider below me.
BetaDivider below me.
GammaNo 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 OneHover me.
Card TwoOr me.
Card ThreeOr 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:
-
<div class="msg"> </div> -
<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 Card BBadge Card C
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
:emptybut 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.
