Using :not()? Try NOT to...
Tags: css
Published: 2020-06-24 02:38:12

<p>CSS is full of little gotchas and head scratchers. It&#8217;s also got a land mine or two that&#8217;s all too easy to step on. One of those landmines is the<code> :not() </code>pseudo-class. </p>


<p>As useful as it may <em>seem</em>, I&#8217;d like to encourage you to <em>not</em> use it, unless you really, <em>really</em> mean to because  of the side-effects it brings.</p>

<h2>What does <code>:not()</code> do?</h2>

<p><code>:not()</code> is a pseudo-class. Like all pseudo-classes, <em>it targets a state</em>. The most common pseudo-classes we see in our stylesheets are <code>:hover</code> and <code>:focus</code>, but we do have other fun friends like <code>:checked</code>, <code>:target</code>, and everyone&#8217;s loneliest, <code>:only-child</code>.  </p>



<p>In the case of <code>:not()</code>, the state that is being target is, well… a negative.<em> It targets a state of not being something</em>. </p>



<p><em>The goal of <code>:not()</code> is to exclude certain elements from a particular ruleset</em>. </p>



<h3><code>:not()</code> tells the browser &#8220;is not this&#8221;</h3>



<p>CSS selectors are parsed right-to-left— but when a pseudo class is &#8220;chained&#8221; to something, the best way to read it is by saying, &#8220;that is also&#8221;. </p>



<p>So we read the below selector as &#8220;every element with the class name <code>article</code>, <strong>that is also not an element with the class name </strong><code> article--news</code>&#8220;</p>



```css
.article:not(.article--news) {
  color: blue;
}
```



<h3>:not() also raises specificity <em>unexpectedly</em></h3>



<p>Pseudo-classes have the specificity of a <em>class</em>.  Usually. </p>



<h4>How a typical pseudo-class behaves</h4>



<p>Styling a plain element (also known as a type selector) gets us a specificity of <samp>0,0,1</samp>:</p>


```css
a {
  color: #6ea5dc;
}
```



<p>Then we style the <code>:hover</code> pseudo-class of this link element, getting the specificity <samp>0,1,1</samp>:</p>



```css
a:hover {
 color: #6ea5c1;
}
```



<p>And if we wanted something special for targeting two states of this element, we&#8217;d get a specificity of <samp>0,2,1</samp>: </p>



```css
a:hover:visited {
  color: #7fb6d2;
}
```



<h4>How  <code>:not()</code> behaves may surprise you</h4>



<p>Let&#8217;s suppose you wanted to target links that are not in a state of being <code>visited</code>. You might write this:</p>



```css
a:not(:visited) {
  color: #6ea5c1;
}
```



<p>The specificity on this selector is <samp>0,1,1</samp>. </p>



<p>Well that&#8217;s just like it was with <code>a:hover</code>. What&#8217;s the big deal?</p>



<p><code><strong>:not()</strong></code><strong> didn&#8217;t raise the specificity. </strong><code><strong>:visited</strong></code><strong> did.</strong></p>



<h2>It&#8217;s the argument within <code>:not()</code> that raises specificity</h2>



<p>Let&#8217;s suppose you&#8217;ve got a special style for your calls-to-action. Maybe they&#8217;re autogenerated and you feel the need to make a single, one-off case of making it different. It might be tempting to write this:</p>



```css
 a:not(#call-to-action--3428){
  color: #333;
}
```



<p>First, let&#8217;s read that out-loud (go ahead, I won&#8217;t judge you):</p>



<blockquote><p>Every <code>a</code> element that is also not an element with the <code>id call-to-action--3428</code></p></blockquote>



<p>Every element. Got it?</p>



<p>Would you believe that the specificity of this style, on <em>every <code>a</code> element</em> is now <samp>1,0,1</samp> ? </p>



<p><code>:not()</code> passes the specificity of the thing you&#8217;re excluding onto the thing your targeting. </p>



<p>I&#8217;ll say that one more time for emotional effect:</p>



<blockquote><p><em><code>:not() </code>passes the specificity <strong>of the element you&#8217;re excluding</strong> to <strong>the element you&#8217;re targeting.</strong></em></p></blockquote>



<p>Ugh.</p>



<h2>Can&#8217;t we just set a best-practice; &#8220;don&#8217;t make the argument for <code>:not()</code> higher than the target?&#8221;</h2>



<p>So we&#8217;re saying <code>a:not(.nav__link)</code> is off the table because this will target every link on the page and give it a specificity of <samp>0,1,1</samp>; that&#8217;s an &#8220;unexpected&#8221; specificity.  </p>



<h3>What if the <em>excluded </em>specificity matches the <em>targeted</em>? </h3>



<p>Isn&#8217;t this safe with a specificity of <samp>0,2,0</samp>?</p>



```css
.nav__link:not(:last-child) {
  color: #444;
}
```



<p>Let&#8217;s read this together:</p>



<blockquote><p>Every element with the class name <code>nav__link</code> that also is not the <code>:last-child</code></p></blockquote>



<p>Every. Element. </p>



<p>Except one—and every element <em>except that one</em> got the specificity of <samp>0,2,0</samp>. </p>



<p>You know how quickly this becomes a problem? Try the first time you add a hover state:</p>



```css
/*If this doesn't come later in the cascade, it's pointless*/
.nav__link:hover { 
  color: #555;
}
```



<h3>Ok, so can the excluded specificity be<em> less than</em> the target&#8217;s specificity?</h3>



<p><code>ul .nav__link:not(:last-child)</code> will get you a specificity of <samp>0,2,1</samp> on every <code>.nav__link</code>, except one. You probably didn&#8217;t mean to do that. </p>



<p><code>.nav__link:not(button)</code>? Specificity of <samp>0,1,1</samp> on <em>every </em> <code>nav__link</code>. You probably didn't mean to do that, either. </p>



<p>The thing is, it doesn&#8217;t matter what the argument is that you send into <code>:not()</code>. Unless it&#8217;s <code>*</code>, it&#8217;s going to raise the specificity on the targeted item. </p>



<p>That&#8217;s right. This is the only thing that you can safely write:</p>



```css
.nav__link:not(*) { /* Pull Request denied*/
  color: transparent; /* double-denied */
}
```



<h2>Should you use <code>:not() </code>to set &#8220;default styles&#8221;?</h2>



<p>I&#8217;ve seen this pattern a few times in my life. Folks write out a &#8220;default style&#8221; using <code>:not()</code>:</p>



```css
.nav__item:not(:last-child) { /* 0,2,0*/
  border-bottom: 1px solid #333;
}
```



<p>But the cost here is that theming is now more difficult. What should be a natural increase in specificity <em>isn&#8217;t</em>. The below styles <em>only work </em>if they come later in my stylesheet. </p>



```css
.nav--darkMode .nav__item { /* 0,2,0 */
  border-bottom: 1px solid #fff; 
}
```



<p>So more than likely, I end up arbitrarily raising specificity (otherwise known as specificity bloat):</p>



```css
.nav--darkMode .nav__item.nav__item { /* 0,3,0 ...who hurt you?*/
  border-bottom: 1px solid #fff; 
}
```



<p>But, if I opt for a less terse selector that has clearer intent:</p>



```css
.nav__item { /* 0,1,0 */
  border-bottom: 1px solid #333;
}

.nav__item:last-child { /* 0,2,0 */
  border-bottom: none;
}
```



<p>We discover that theming can come without introducing dependencies on code organization:</p>



```css
.nav--darkmode .nav__item { /* 0,2,0 */
  border-bottom: 1px solid #fff;
} /* Pull Request Approved. Let's get beer. */
```



<h2>So when is <code>:not()</code> safe?</h2>



<p><em>Rarely</em></p>



<p>This is really an article about <em>selector intent.</em> Most of the time we write code thinking about what we <em>intend</em> to style. Not what we <em>don&#8217;t</em> intend to style. </p>



<p><code>:not()</code> is safest when you&#8217;ve reached an edge case. An edge-case is a point in your code where the scope is very small and you have a clear understanding that it won&#8217;t be expanded further.  </p>



<p>A perfect example is picking up where we left off with a themed navigation item. In our last example, we got ourselves in a hole: we would have to remove border on the<code> last-child</code> again:</p>



```css
.nav--darkmode .nav__item:last-child { /* 0,3,0 */
  border-bottom: none:
}
```



<p>That <em>isn&#8217;t</em> what we want. We don&#8217;t want to write code that makes us write more code. </p>



<p>Now, suddenly, <code>:not()</code> seems very useful:</p>



```css
.nav--darkmode .nav__item:not(:last-child) {
  border-bottom: 1px solid #fff;
}
```



<p>We still raised the specificity (<samp>0, 3, 0</samp>) on our <code>.nav__item</code>. But we did it on an edge-case at the benefit of not having to rewrite a pre-existing style. We wrote our selector with a clear intent of, &#8220;I don&#8217;t want to overwrite <code>:last-child</code> <strong>except when</strong> I&#8217;m dealing with a <code>.nav__item</code> that&#8217;s within a <code>.nav--darkmode</code> container.&#8221;</p>



<h2>Avoid <code>:not()</code>, but don&#8217;t ignore it</h2>



<ul><li>Avoid it for excluding type selectors (elements)</li><li>Avoid it for setting default styles</li><li>Never pass in an element with a higher specificity than your target</li><li>Prefer it in narrow scopes to address edge-cases</li><li>Prefer it when it saves you from duplicating code at a higher specificity</li></ul>