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

tl;dr: 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.

The CSS pseudo-class in action. Please note: The screenshot shows compiled CSS for clarity; we actually make heavy use of SCSS features like mixins and nesting to keep project code DRY and maintainable.

When it comes to CSS and code management, I 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. My 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 me to write styles with the cascade, where I 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, I 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 my belt for this endeavor. Without it, I used to declare rules for properties like font-size, line-height, color, font-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.

I mostly work with content-heavy websites that are powered by a content management system (CMS). 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 I don't relish the thought of resetting margin or list-style for every non-content kind of <ul> I want to use in my templates (i.e. for navigation or a tabs component). In the past, I solved this by wrapping any WYSIWYG content in a <div class="wysiwyg">…</div> and used the selector to apply my 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 I 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 me to create an additional class with the same styles I 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 I 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.

I do not know why it took me so long to realize this, but I 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, I can reset all my
// 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 my problems vanish into thin air:

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

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

What do you think?