The New !important

I spotted this CSS used in a very popular WordPress theme:

.wp-block-theme-button .button:not(.specificity):not(.extra-specificity)

Those .specificity and .extra-specificity classes sure pop off the page, don’t they? I wonder what they do. Asking the author directly would be a good way to find out, but who wants facts when we can make wild assumptions and project our own opinions on it. After all, that’s what the Internet is founded on!

Perhaps :not() is a way of selecting .button elements contained in a parent .wp-block-theme-button wrapper while excluding any .button that is assigned the .specificicity or .extra-specificity classes that are being used to make more aggressively styled buttons in specific contents.

/* Specificity: 0 1 0 */
.button {}

/* Specificity: 0 2 0 */
.button.specificity {}

/* Specificity: 0 3 0 */
.button.specificity.extra-specificity {}

What a novel and clever strategy! We’re effectively talking about utility classes that contain no styling and are used purely for bumping up a selector’s specificity when needed. Specificity is low by default but can be made incrementally stronger by adding one or both of the specificity classes.

Is it a good idea, though? I dunno, that’s for you to decide. If it fits your mental model and gets the job done, then hey, go bananas. CSS is poetic in the sense that it can say the same thing in many different ways. This is just another way of increasing a selector’s priority in the Cascade.

I’ll bet the lint in my pocket that at least one of y’all sees this approach as nothing more than an elaborate stand-in for !important. Maybe you were already starting a snarky snippet like this:

.button.important {}
/* or */
.button.very.important {}

Or, even better, using fake IDs in the :is() arguments to artificially boost the score:

/* Specificity: 1 0 0 πŸ’ͺ */
:is(.button, #important) {}

Again, I ain’t gonna poo on anyone’s way of going about things. There’s nothing in the rules that says this is invalid.

(Wait, there are rules for writing CSS?!)

I will, however, go on record saying there may be a happier path for managing specificity than using artificial classes and IDs this way (or even at all). Like, isn’t this a pretty ideal use case for @scope? Scoped styles have this real neat way of setting where a style starts and ends called scoped limits.

@scope (.card) to (:scope > .button) {
  /* Style rules go here. */
}

Check that out: after defining the scoped root, .card, we dropped in the to keyword to define the scoped limit, .button. If this is our markup:

<article class="card">
  <img src="image.avif" alt="...">
  <h2>Card Heading</h2>
  <p>Card content.</p>
  <a class="button" href="#">Read Full Story</a>
</article>

…then CSS knows that the .card class is the scoped root where styles begin and to stop applying those styles when it reaches a .button class in that scope. If we had not determined the scoped limit, then we would have run into a possible styling collision where any of the card’s other styles may have inadvertently match the button, say:

.card a {
  color: hsl(25, 100%, 50%);
}

That will never touch the .button since we explicitly instructed the selector to keep it out of scope. CSS is just so gosh-darned smart like that these days that it’s possible you will never reach for !important again. (You may say you already don’t, but we all make exceptions.)

Between pseudo selectors that influence specificity, scoped styles, Cascade layers, and formatting strategies like BEM, the Cascade is more manageable than ever. And there’s no prescription for how it is, ahem, specifically done. I just love seeing how others handle situations and questioning how I might’ve gone about it. Not to feel better about myself, but challenge my own understanding and way of thinking.

✏️ Handwritten by Geoff Graham on April 24, 2024

6 Comments

  1. # April 24, 2024

    @geoff I use :not(##) as a specificity hack to avoid using !important, it’s a little more aggressive but I try to be careful about using it.

    Also, unless I’m misunderstanding you, :not() does not behave like :where(), and indeed does add specificity, by the way! a:not(b) has a score of 0,0,2, not 0,0,1: https://polypane.app/css-specificity-calculator/#selector=a%3Anot%28b%29
    CSS Selector Specificity Calculator | Polypane

    Reply
  2. # April 24, 2024

    @chriskirknielsen Ahhhhh thanks! I got mixed up on that last point.

    Yeah, it’s an approach I’m not terribly comfortable using, but thought it was interesting to see it in the wild in such a popular project.

    Reply

Leave a Reply

Markdown supported