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 -

  1. What kinds of selectors are used. There are three kinds of selectors -
    1. ID Selectors, e.g. #bar
    2. Class Selectors (including Pseudo-Classes), e.g. .foo or :last-child
    3. Type Selectors, e.g. p
  2. 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 all p'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

Keep in touch KeepinTouch

Subscribe to my Newsletter 📥

Receive quality articles and other exclusive content from myself. You’ll never receive any spam and can always unsubscribe easily.

Elsewhere 🌐