Published:

Clean up Your CSS with the :not() Selector.

Poor CSS. It somehow always manages to be the butt of at least one joke in every programmer’s repertoire. Heck, they even enjoy making memes about it over on Reddit.

Personally, I like to think of those three letters more like the slightly misunderstood individual at your school or work who you find is actually super awesome once you get to know them—maybe Kyle is a chainsaw juggler in his spare time with discerning opinions on Led Zeppelin, but you’ll never know until you go speak to him!

It’s easy to misuse the “cascading” part of CSS, especially as projects grow in size and scope. While awesome in theory, it can quickly lead to selector creep and unnecessary resets. But it does :not() have to be this way (see what I did there?). There are many ways that :not() can help us stave off these issues.

Reduction via Negation

When HTML5 came onto the scene, it provided us with a whole new set of form input types (yay!). These new inputs greatly improve user experinece, but they also mean having a larger number of elements to manage. The most straightforward way to address this would be to lump all of our new inputs with the old:

input {
  &[type="url"],
  &[type="tel"],
  &[type="date"],
  &[type="text"],
  &[type="time"],
  &[type="week"],
  &[type="email"],
  &[type="month"],
  &[type="search"],
  &[type="number"],
  &[type="password"],
  &[type="datetime"],
  &[type="datetime-local"] {
    /* So many inputs to style... */
  }
}

Taking a look at this list, what we’re really trying to do is target any form inputs that appear/behave similar to input[type="text"] (i.e. not a checkbox, radio, submit, et cetera). We can vastly simplify our styling by utilizing a blacklist instead of the whitelist above:

input {
  &:not([type="checkbox"]):not([type="radio"]):not([type="submit"]) {
    /* Much better. :) */
  }
}

We could certainly clean this up a lot more by abstracting away our text-centric input styles into a class to be applied as needed; however, this hits a snag if you find yourself integrating a library into your project that doesn’t offer full markup access.

Don’t let your imagination stop there as this technique can be applied to many different scenarios. I really enjoyed Dave Rupert’s write-up on targeting specific elements within a post’s content using *:not(). It’s an approach I’d love to explore in future projects—but with caution, as mentioned towards the end of the article.

More Efficient Child Styles

One of my favorite ways of employing the :not() selector is pairing it with :first-child and :last-child, which can help to eliminate superfluous resets like:

nav {
  > a {
    border-left: 1px solid #f5f5f5;

    &:first-child {
      border-left: 0; /* Get outta here! */
    }
  }
}

In the above example our aim is to place a 1px dividing border between each navigation item. The issue is that our first <a> will have an unintended border to the left the way it’s currently styled (hence the removal using :first-child). While the preceeding bit of styling gets us where we want to go, it could be cleaned up and more clearly stated with the following:

nav {
  > a:not(:first-child) {
    border-left: 1px solid #f5f5f5; /* Much better. :) */
  }
}

It’s a small change that adds up over time as well as making the intent of your styles more readily apparent. I use this a lot when styling the spacing between typographic elements. I tend to space things out with bottom margins, while adding larger top margins to headings to help visually group sections together. Previously, I may have done that like so:

h1, h2, h3, h4, h5, h6 {
  margin-top: 2.5rem;
  margin-bottom: 0.5rem;

  &:first-child {
    margin-top: 0;
  }

  &:last-child {
    margin-bottom: 0;
  }
}

p, ul, ol {
  margin-bottom: 1.5rem;

  &:last-child {
    margin-bottom: 0;
  }
}

These days, I’m more inclined to use the :not() technique:

h1, h2, h3, h4, h5, h6 {
  &:not(:first-child) {
    margin-top: 2.5rem;
  }

  &:not(:last-child) {
    margin-bottom: 0.5rem;
  }
}

p, ul, ol {
  &:not(:last-child) {
    margin-bottom: 1.5rem;
  }
}

Things to Keep in Mind

The first tasty nugget of information regarding :not() is that chaining multiple selectors together is permitted:

p:not(.class-a):not(.class-b) {
  /* Styles here... */
}

However, nesting is not permitted (i.e. p:not(:not(...))). While I can’t think of a situation where that would actually be useful, I’d certainly love to hear any ideas out of sheer curiosity. The nesting restriction was brought to my attention in this CSS-Tricks article while doing a little research for this post.

Pseudo-elements are also not permitted, so no p:not(::first-letter) business in your style sheets.

Regarding specificity, :not() itself is not considered part of the specificity calculation, but the selectors placed inside are.