CSS :not() selector

As you probably know, CSS selectors are patterns that can be used to apply a group of style declarations to multiple elements. For example, if you want to select all elements with the class foo, you can use the selector .foo or [class~=foo]. But sometimes, instead of styling the elements that have a specific characteristic, we want to exclude these and style the other ones.

For example, some developers prefer the border box sizing model rather than the content box one, so they use

* { box-sizing: border-box }

But then, imagine there is an image like

<img src="img.png" width="100" height="50" style="padding: 5px" />

The size attributes will include the padding, so the resulting content area will be 90x40. Not only will the image be downscaled, possibly producing artifacts, but it will also be stretched: the aspect ratio becomes 2.25 instead of the natural 2.

The above shows that excluding images can be a good idea. In CSS2, the solution was simply using a more specific selector to undo the general one:

* { box-sizing: border-box }
img { box-sizing: content-box }

However, in more complex cases this can get tedious, and the reset value may not be that obvious. Therefore, the Selectors Level 3 specification introduced the :not() pseudo-class. It accepts a selector argument, and matches all elements that do not match the argument. The example above can then be

:not(img) { box-sizing: border-box }

There were some limitations though. The selector argument had to be a simple selector, that is, one of:

Additionally, nesting negations like :not(:not(...)) was also invalid.

:not() with selector list argument

Selectors level 4 has made :not() more flexible, now it accepts any selector list as the argument.

In particular, it allows a compound selector argument, like :not(ul[reversed]), which matches all elements except the ones that both are ul and have the attribute reversed.

Another example of what you can do in level 4 is using a complex selector, that is, a sequence of compound selectors separated by combinators. For instance, :not(div > p) matches all elements except the p which have a div parent.

And finally, an example with a selector list could be :not(div, p), matching all elements except the ones that are either a div or a p.

Is that completely new behavior? Well, using De Morgan’s laws, the selectors above can be transformed into others which were valid in level 3:

Level 4 new syntax Level 3 alternative
:not(ul[reversed]) :not(ul), :not([reversed])
:not(div > p) :not(p), :not(div) > p, p:root
:not(div, p) :not(div):not(p)

However, the specificities are not completely equivalent. The specificity of a selector is a tern of 3 natural numbers (A,B,C), where in simple cases A is the number of ID selectors in the whole selector, B is the number of attribute or class selectors and pseudo-classes, and C is the number of type selectors and pseudo-elements. The specificity is one of the criteria used to solve conflicts when different CSS rules set the same property to the same element: the winning declaration will be the one with the most specific selector, when comparing specificities in lexicographical order.

The specificity of a :not() pseudo-class is the greatest among the specificities of the complex selectors in the argument, while the specificity of a selector list is the greatest among the complex selectors that match.

For example, for :not(ul[reversed]), the specificity is the same as for ul[reversed], i.e. (0,1,1). However, the specificity of :not(ul), :not([reversed]) can either be:

  • If only :not(ul) matches: (0,0,1).
  • If only :not([reversed]) matches: (0,1,0).
  • If both match, the maximum: (0,1,0).

Additionally, the level 3 alternatives can become cumbersome when used as part of a bigger selector. For instance, :not(.a1.a2) :not(.b1.b2) :not(.c1.c2) would become

:not(.a1) :not(.b1) :not(.c1), :not(.a1) :not(.b1) :not(.c2),
:not(.a1) :not(.b2) :not(.c1), :not(.a1) :not(.b2) :not(.c2),
:not(.a2) :not(.b1) :not(.c1), :not(.a2) :not(.b1) :not(.c2),
:not(.a2) :not(.b2) :not(.c1), :not(.a2) :not(.b2) :not(.c2)

Note the combinatorial explosion! It could be avoided using :is() or :where(), but they are also new level 4 additions.

Moreover, not all :not() selectors have a finite alternative in level 3. Consider :not(div p), that is, all elements which either are not p or don’t have any div ancestor. The problem is that we can’t directly enforce a constraint over all ancestors, instead we need something like

:not(p), p:root,
:root:not(div) > p,
:root:not(div) > :not(div) > p,
:root:not(div) > :not(div) > :not(div) > p,
/* ... */

and if a priori the DOM tree can be arbitrarily deep, we don’t know when to end the selector.

It’s for all these reasons that allowing selector lists in :not() is such a nice addition!

Browser support and Igalia’s Open Prioritization

If the new capabilities of :not() with a selector list sound cool to you, you might want to start using it right now! However, while major browsers have supported the level 3 version for a long time, only WebKit supports the level 4 one, Chrome and Firefox don’t.

But here is where Igalia comes into play! We are happy to announce Open Prioritization, an experiment for crowdfunding web platform features.

Historically, which features are implemented sooner and which ones are delayed has been decided by browser vendors, and big companies that can fund the work. But wouldn’t it be great if web developers could also have a saying in this prioritization?

At Igalia we have selected a few tasks that we think the community might be interested in. During a first stage, anybody can pledge their desired amount of money for their preferred choices. The first feature that reaches the goal will be selected for a 2nd stage, in which the actual funding will happen. And once funded, Igalia will do the implementation work. Note: don’t worry if you pledge for an option which is not selected, your money is only deducted when funding. See the FAQ for more details.

One of the features that we offer is precisely implementing :not() with selector lists in Chrome. If you like the idea, I invite you to pledge here!

Open Prioritization by Igalia. An experiment in crowd-funding prioritization.