Alex Vipond
SHIFT + D

Styling solution tier list

Published 8 months ago

S
bg-white
A
A tasteful :where(:not(...))
B
!bg-white
C
[.simple-arbitrary-variant&]:!bg-white
D
E
[.complex-arbitrary-variant_:not(#very-nice-to-write)>[data-high-specificity=true]_&]:!bg-white
F
Fixing overly specific CSSWriting new, more specific CSS

Legacy projects tend to ship big piles of nested SCSS. If you can wire up Tailwind in these projects, it's a huge win, for all the usual reasons.

But once you get Tailwind working, you're still likely to run into all kinds of problems where Tailwind classes can't override highly specific legacy styles.

There are lots of solutions, but they all have different impacts on maintainability, dev effort, browser support, and scope creep. Based on those factors, I ranked every solution I could think of.

Let's style a p tag!

S tier

Plain Tailwind utility classes with no important modifier ! are always the best and should always be the first choice.

<div id="page">
  <main>
    <div class="some-godforsaken-class-name">
      <p class="bg-white">My paragraph</p>
    </div>
  </main>
</div>

A tier

In some cases, you can add a tasteful :where(:not(...)) to legacy SCSS to exempt certain elements from legacy styles. Since :where() adds zero specificity points, there's no risk of unintentionally breaking distant parts of the UI that rely on these styles.

I like to do this with data attributes:

<div id="page">
  <main>
    <div class="some-godforsaken-class-name">
      <p
        data-not-me
        class="bg-white"
      >My paragraph</p>
    </div>
  </main>
</div>
/* The bowels of a legacy SCSS file */
#page {
  main {
    & > .some-godforsaken-class-name {
      p:where(:not([data-not-me])) {
        background-color: fuchsia;
      }
    }
  }
}

The :where(:not(...)) is low effort, prevents scope creep, and is relatively maintainable, but it only works in browsers that support :where().

B tier

Next up is Tailwind utility classes with an important modifier !, which can create maintainability problems in some cases, but meet all our other requirements:

<div id="page">
  <main>
    <div class="some-godforsaken-class-name">
      <p class="!bg-white">My paragraph</p>
    </div>
  </main>
</div>

C tier

Tailwind utility classes with simple arbitrary variants are arguably better than !important, since they don't raise specificity as much, but I rank them a little lower because they can be harder to read and write, and more brittle if the surrounding markup changes. It's also more rare that they'll actually work.

Case in point—simple arbitrary variants wouldn't work for the code example I've been leaning on so far, so here's a slightly different example where they'd be appropriate:

<div id="page">
  <p class="my-paragraph [.my-paragraph&]:bg-white">My paragraph</p>
</div>
#page p {
  background-color: fuchsia;
}

#page p has the same specificity as the compiled output of [.my-paragraph&]:bg-white, so as long as your Tailwind stylesheet is loaded after the legacy stylesheet, the Tailwind class will be further down the cascade, and it will win.

Arbitrary data attribute variants behave similarly—I consider them to be in this same category of simple arbitrary variants.

E tier

I'm jumping over D tier to emphasize that we're getting into pretty cursed territory here!

If !important and simple arbitrary variants don't work, you can try complex arbitrary variants. These are even harder to read and write, and even more brittle than simple arbitrary variants.

At this point, you're basically just writing legacy CSS. The one small advantage is that you're doing it in a Tailwind class that's colocated with the element it's affecting, instead of smushing new SCSS into a legacy file.

<div id="page">
  <main>
    <div class="some-godforsaken-class-name">
      <p class="[#page_main>.some-godforsaken-class-name_p&]:bg-white">My paragraph</p>
    </div>
  </main>
</div>

F tier

Tied for last place are truly bleak solutions: fixing root causes in the legacy CSS, or adding new CSS to the legacy file.

Fixing root causes is obviously preferable when it's possible, but in bigger, older codebases, you really have a 50/50 chance of getting it done without massive scope creep and unrelated, uncaught bugs.

Adding new CSS is just...sad.

#page {
  main {
    & > .some-godforsaken-class-name {
      p {
        background-color: fuchsia;

        &.my-paragraph {
          background-color: white;
        }
      }
    }
  }
}

ON THIS PAGE

Styling solution tier listS tierA tierB tierC tierE tierF tier