A love letter to the CSS :not() pseudo-class

The use of :not([class]) as an enhancement for HTML element selectors in ITCSS' "elements" layer cancels the need for overriding rules in more specific layers.

Written by: Søren
Published on: 2017-11-24

When it comes to CSS and code management, we have long since made the move to BEM and ITCSS with a smattering of Functional CSS. If you haven't heard of one or any of those, have a look at the following articles (familiarity with ITCSS at least is sort of required for the rest of this post):

BEM:

ITCSS:

Functional CSS:

Done? Splendid. Onwards!

As you (now) know, ITCSS was created by Harry Roberts and is essentially a system that helps you to embrace the cascade (the "C" in CSS) and avoid writing convoluted selectors with ever increasing specificity (often referred to as "specificity hell" or "specificity wars") as projects and/or teams grow. ITCSS solves this by organizing your styles in layers from very generic to very specific. Our typical setup for a SCSS-based project follows Harry's layer suggestions quite closely:

  • Settings – Variables for breakpoints, colors, font sizes, …
  • Tools - Mixins & functions
  • Generic - Baseline rules like normalize/reset and box-sizing
  • Elements - Baseline rules for HTML elements, a bit like normalize++
  • Objects - Abstractions and patterns like media object, grids etc.
  • Components - Discrete parts of the UI like buttons and and links, but also composites like accordions, main navigation, page header etc.
  • Trumps - Very specific overrides, mostly functional helper classes for layout (margins, paddings)

Among those, the "elements" layer is dedicated to styling pure HTML element selectors, while the code in the following layers uses classes exclusively (HTML element selectors should be avoided here to keep specificity low).

ITCSS helps us to write styles with the cascade, where we embrace the fact that rules of the same specificity in a lower layer – aka "later" in a concatenated production file - will overwrite earlier rules. However, we make an effort to use overwrites sparsely and avoid complexity (or, as many CSS developers have come to call it, "dark magic").

The  :not() pseudo-class is the newest tool in our belt for this endeavor. Without it, we used to declare rules for properties like  font-sizeline-heightcolorfont-weight and especially  margin in the "elements" layer (i.e. for headings or lists), only to reset them later in the "components" layer for BEM-elements like  news__title or  accordion__header that use an appropriate HTML element and not a  <div>. This could be a heading, its level depending on the document outline (i.e.  <h3>), but it would not necessarily look like a third-level heading.

"Then why do you style classless HTML elements in the first place?", you ask? Excellent question! Let me digress a little.

At webfactory, we help create content-heavy websites that are powered by our content management system (CMS) wfDynamic. Content editors (the people) usually work with so-called WYSIWYG editors (the software) that support text formatting from italic and bold to inserting different types of headlines, lists, tables, links, etc. If you have worked with WYSIWYG editors before, you will know that the good ones produce well-formed, semantic HTML like  <ul><li>…</li></ul> for unordered lists or  <h2>…</h2> for sub-headings – HTML without classes.

Obviously, this WYSIWYG content needs to look just as right and correspond to the project's design guidelines as more "handcrafted" components like news teasers or FAQ accordions, where the content is directly retrieved from a database and marked up in a view template by a developer.

This poses a dilemma: It's necessary to style classless HTML elements so WYSIWYG content looks great, but at the same time we don't relish the thought of resetting  margin or  list-style for every non-content kind of  <ul> we want to use in our templates (i.e. for navigation or a tabs component). In the past, we solved this by wrapping any WYSIWYG content in a  <div class="wysiwyg">…</div> and used the selector to apply our styles contextually, like so:

.wysiwyg {
    h2 { … }
    ul { … }
}

// Note: This kind of rule nesting is a feature provided by SCSS.

The above is a perfectly valid method, but it never felt right. What if we have a design for lists or headings that applies to a majority of all lists or headings with only a few exceptions? The contextual approach requires us to create an additional class with the same styles we used for the .wysiwyg-selector that can be applied to handcrafted components like  news__title. Again, this is a perfectly valid technique which can be made more maintainable by including a Sass mixin in both use cases (which in turn negates the necessity of a specific class). And still we believed there had to be an easier way that required less code. And there is!

Enter the  :not() pseudo-class

From MDN web docs:

The  :not()  CSS pseudo-class represents elements that do not match a list of selectors. Since it prevents specific items from being selected, it is known as the negation pseudo-class.

We do not know why it took us so long to realize this, but you can specifically target HTML elements without a class by simply applying  :not([class]) to them.

Here's a simple example of how this looks in a current project:

h2:not([class]) {
    color: #555555;
    font-size: 24px;
    font-weight: 300;
    line-height: 1.2;

And a more complex one:

// Due to design and implementation choices, we can reset all our
// unordered lists to a common baseline
ul {
    list-style: none;
    margin-top: 0;
    margin-bottom: 0;
    margin-left: 0;
    padding-left: 0;
}

// All classless unordered lists have an orange bullet and
// are slightly spaced (by applying a margin-top to
// consecutive list items)
ul:not([class]) {
    li {
        line-height: 1.375;
        padding-left: 1.15em;
        position: relative;

        &::before {
            content: '•';
            color: #ff8c00;
            font-size: 1.5em;
            line-height: 1;
            position: absolute;
            top: 0;
            left: .1em;
    }

    + li {
        margin-top: 5px;
    }
}

Suddenly, all our problems vanish into thin air:

  • we can style WYSIWYG content without a contextual class
  • we do not have to overwrite any unwanted rules for our handcrafted components with classes
  • we can benefit from the generic rules for HTML elements whenever we want, simply by omitting to apply a class to them

We have used this approach for the better part of three months now and not encountered any downsides yet.

What do you think?

Interesse geweckt?

Wir hören gerne zu, wenn Sie Fragen oder Anmerkungen zu diesem Thema haben. Und wenn Sie ein Projekt, ein Produkt, ein Problem oder eine Idee mit uns besprechen möchten, freuen wir uns erst recht über ein Gespräch!