On :not and Specificity
The negation pseudo-class, :not
, can be incredibly useful. It allows us to target elements based on what attributes they don't have, rather than what they do. This helps us avoid writing extra, increasingly specific, rules in an attempt to override previous ones.
A common example of this is when we want to apply a style to all list items, expect the last one. For example -
/* Without :not */
li { border-right: 1px solid #000; }
li:last-child { border-right: none; }
/* Using :not */
li:not(:last-child) { border-right: 1px solid #000; }
The :not
pseudo-class is one I use a lot. However, in my past use, I would occasionally run into scenarios in which the :not
declaration would override a positive one. For example -
a:not(.ul) { text-decoration: none; }
nav a { text-decoration: underline; }
When I did this, I found that the nav a
elements would still not have an underline. I was initially a bit baffled as to why this would happen, until I did more reserach into how the :not
rule actually works, and it's effect on specificity.
Specificity 101 #
Specificity in CSS can be complicated to grasp, so it's best to start with an example. Consider the following element -
<p class="foo" id="bar">Lorem ipsum dolor sit amet.</p>
And the following styles -
p { color: red; }
.foo { color: green; }
#bar { color: blue; }
p.foo#bar { color: yellow; }
In cases like this where there are multiple selectors targeting the same element, which selector's rules will prevail is determined by its specificity. The specificity of a selector is determined by 2 things -
- What kinds of selectors are used. There are three kinds of selectors -
- ID Selectors, e.g.
#bar
- Class Selectors (including Pseudo-Classes), e.g.
.foo
or:last-child
- Type Selectors, e.g.
p
- ID Selectors, e.g.
- The number of each kind of selector. For example if there are 2 IDs and 3 Classes.
These two factors combined determine the level of specificity for a selector. Using the example above, this is how specificity is calculated for each selector -
Selector | No of IDs | No of Classes | No of Types | Specificity | Winner |
---|---|---|---|---|---|
p |
0 | 0 | 1 | 0-0-1 | |
.foo |
0 | 1 | 0 | 0-1-0 | |
#bar |
1 | 0 | 0 | 1-0-0 | |
p.foo#bar |
1 | 1 | 1 | 1-1-1 | ✓ |
 **The way to read the resulting specificity is not to read it as one number, but to consider each unit itself, from left to right**. |
When comparing two selectors, we take the first value (representing the number of ID Selectors) and compare that. If one selector has a higher ID value, then it automatically wins the specificity battle. If, and only if, the two selectors are equal in that value, do we move on to the next value (representing the number of Class Selectors), and so on.
Consider the following examples -
Selector | No of IDs | No of Classes | No of Types | Specificity | Winner |
---|---|---|---|---|---|
p |
0 | 0 | 1 | 0-0-1 | |
p:last-child |
0 | 1 | 1 | 0-1-1 | |
p.foo.bar.baz |
0 | 3 | 1 | 0-3-1 | |
#bar |
1 | 0 | 0 | 1-0-0 | ✓ |
 **A single ID will always beat 100 classes**, because the number of classes is irrelevant if IDs exist. |
(As a side note, it is important to remember that these calculations only apply to styles defined in CSS. Inline styles override all selector-based styles.)
What about :not? #
The :not
itself doesn't add anything to the specificity number as other pseudo-classes do. However, the selectors within the :not
do.
With respect to specificity, adding
p:not(.foo)
, is essentially the same as adding.notFoo
to allp
's that do not have the the class of.foo
Fo example -
p.bar { color: red; }
p:not(.foo) { color: green; }
What colour would you expect <p class="bar">
to be? The correct answer is green, not red.
This is because, by adding the the :not
rule, we have essentially added a class .notFoo
to our <p class="bar">
element.
Selector | No of IDs | No of Classes | No of Types | Specificity | Winner |
---|---|---|---|---|---|
p.bar |
0 | 1 | 1 | 0-1-1 | Equal |
p:not(.foo) |
0 | 1 | 1 | 0-1-1 | Equal |
 The `:not` rule is now the same level of specificity as the positive class element. So, because it is defined later in the CSS, it prevails. This also explains why, in the `nav` example, the `:not` rule won over the nested type selectors. |
Selector | No of IDs | No of Classes | No of Types | Specificity | Winner |
---|---|---|---|---|---|
a:not(.ul) |
0 | 1 | 1 | 0-1-1 | ✓ |
nav a |
0 | 0 | 2 | 0-0-2 |
Things get even more messy when we introduce IDs. The same way that p:not(.foo)
is almost like adding .notFoo
to all <p>
elements, adding p:not(#bar)
is like adding #notBar
to all <p>
elements. For example -
p:not(#foo) { color: green; }
p.bar { color: red; }
What colour would you expect <p class="bar">
to be? The correct answer is green, not red.
Selector | No of IDs | No of Classes | No of Types | Specificity | Winner |
---|---|---|---|---|---|
p.bar |
0 | 1 | 1 | 0-1-1 | |
p:not(#foo) |
1 | 0 | 1 | 1-0-1 | ✓ |
 Even though our positive class element is defined later in the css, we have essentially inadvertently added an ID to the element from the `:not` rule! 😱 |
Using :not #
This effect of :not
has made me re-think the way I use it. Even though it feels like we are bypassing the need to write increasingly specific rules to override others, like in the li:last-child
example, it seems like :not
inadvertently does the same thing.
I will definitely keep using :not
because, in many circumstances, it is still the cleaner way to write styles. However, I will use it with a few caveats -
- Never use it with IDs, e.g.
:not(#bar)
- Restrict using it with generic type selectors, e.g.
div:not(.foo)
- Define
:not
rules earlier in CSS so they can be overridden if necessary