First installment of a series of mini lessons I learn while growing into an actual full stack software engineer on the job. Today: Tailwind specificity and why overriding classes doesn't work.

The idea of this series is that I document and consolidate what I learn, by writing it out. Hopefully, it will also help out a few other people in the process.

First lesson is about Tailwind, the CSS-framework we work with at Crash (the company I work for).

Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override.
Tailwindcss.com

I already had a little experience with UI frameworks like Bootstrap and Bulma, but Tailwind works a little differently. Whereas other frameworks tend to come with a lot of default styling out of the box (like button and card classes), Tailwind only provides utility classes like text-lg and md:w-24.

The downside of this is obviously that you have to do a lot of the styling yourself, but the upside is that you don't have to battle your framework, fighting specificity wars to override default classes whenever you want custom styles – which, in the case of Crash is all the time.


Situation:

I was building a new feature that required a button that was styled to look like a simple text link. So I imported our custom Button React Component and passed it the text option under the appearance prop, that was supposed to make it look like plain text. So far so good.

The only problem was that the way this Button with appearance='text' was styled, my button's text ended up exactly one pixel higher than all the actual links it was right next to.

So naturally, having experience with Bootstrap, Bulma and CSS in general, I tried to override the classes that caused this with specificity. But then I ran into the oddest problems: with some classes, this would work, but for others it wouldn't. 😧


{showArchiveLink && (
       <>
         <span>•</span>
         <Button
           type="button"
           unstyled
           className="hover:underline leading-3 justify-center"
           onClick={() => archivePitch({ variables: { id: pitch.id } 					})}
          >
            Archive Pitch
          </Button>
       </>
 )}


I tried to find some common denominator, but I couldn't find out why, until our lead developer Dave explained it to me.

Lesson:

Trying to override classes with specificity doesn't work in Tailwind because all Tailwind's classes have a specificity of one. This means that the definition order determines which class wins out in the case of conflict: whichever one was defined later applies.

This explains why trying to override classes with other classes would work sometimes, but not all the time.

Rather than overriding classes with specificity, like you do in other frameworks, Tailwind actually forces you to actively remove classes in cases like this.

So that's what I ended up doing. I rewrote the Button component and its styles to account for a completely un-styled option, then imported that.

Although annoying in the moment, this behavior by Tailwind actually enforces good design patterns in this way, so it's a smarter idea for the long term.

Score 1 for Tailwind.