A few years back, I wrote a short post on specificity, element proximity, and the negation pseudo-class. Everything in it is still accurate and relevant, but I have some updates to share.

First off, I’d like to clarify something that some people may have found confusing. In that post, I said:

But it turns out that the negation pseudo-class isn’t counted as a pseudo-class.

That might leave some people with the idea that the entire negation portion of the selector is ignored for the purposes of specificity, especially if you don’t speak spec.

So consider the following:

div:not(.one) p

In order from left to right, that’s an element selector ( div ), a negation pesudo-class ( :not ) a class selector ( .one ), and another element selection ( p ). Two element selectors and one class selector are counted towards the specificity, yielding a total of 0,0,1,2 . That’s the same specificity as div.one p , though the two selectors select very different things.

In Ye Olden Days, that was easy enough to work out, because :not() could only ever contain a simple selector. Things are looking to get more complicated, however— :not() is set to accept grouped selectors. So we will at some point be able to say:

div:not(.one, .two, #navbar) p

So any p element that is not descended from a div that has a class containing either one or two (or both), or that has an id of navbar , will be selected.

But how do we calculate the specificity of that whole selector? Just add up all the pieces? No. The Working Group recently decided that the specificity contributed from inside a :not() will be equal to the single selector with the highest specificity. So given div:not(.one, .two, #navbar) p , the #navbar will contribute 0,1,0,0 to the overall specificity of the selector, yielding a total of 0,1,0,2 . The specificities of .one and .two are ignored.

This same approach will be taken with the :has() and :matches() pseudo-classes. Thus we get the following:

:matches(nav, header, footer#pageend) a[href] {color: silver;} /* 0,1,1,2 */ article:has(a.external, a img) /* 0,0,1,2 */ input:not([type="radio"], [type="checkbox"]) /* 0,0,1,1 */

In the first instance, the bits that are added together are footer#pageend and a[href] , so that’s one ID, one attribute, and two elements. In the second, it’s article and a.external for one class and two elements. And last, we add up input and either of the [type=""] attribute selectors, since their specificities are equal, which means we add up one attribute and one element.

There is still, so far as I’m aware, no concept of DOM-tree proximity in CSS. I would still continue to wager that will remain true, though I’d put a fair bit less money down now than I would have six years ago.