Overriding Tailwind classes in React

diogoko

Diogo Kollross

Posted on May 18, 2022

Overriding Tailwind classes in React

EDIT: A previous version of this article mentioned tailwind-override, but this package has been replaced with a more complete library that merges more Tailwind classes.

The problem

Imagine you create a simple React component that displays a pre-styled blue button using Tailwind CSS and allows adding more classes to customize it.



function Button({ label, className, ...props }) {
  const classes = `
    border
    border-black
    bg-blue-600
    p-4
    rounded-lg
    text-white
    text-xl
    ${className ?? ""}
  `;
  return <button className={classes}>{label}</button>;
}


Enter fullscreen mode Exit fullscreen mode

You can use it as:



<Button label="Hello" />


Enter fullscreen mode Exit fullscreen mode

Button with blue background
Default blue button styling

And it works as advertised. Now you want to change its color to red:



<Button label="Hello" className="bg-red-600"/>


Enter fullscreen mode Exit fullscreen mode

Button with blue background
Customized red button... wait!?

What just happened? I added the new CSS class to className, so let's check if it's actually included in the rendered HTML:



<button class="
    border
    border-black
    bg-blue-600
    p-4
    rounded-lg
    text-white
    text-xl
    bg-red-600
  ">Hello</button>


Enter fullscreen mode Exit fullscreen mode

It's right there at the end - bg-red-600, and it comes after bg-blue-600. A class should override anything that came before it, right?

Wrong.

The cause

It turns out that the space-separated CSS class list that the class HTML attribute accepts is not treated as a list when calculating CSS rules precedence by the browser. The class attribute actually contains the set of classes the element has, so the order doesn't matter.

This problem is not specific to Tailwind. It can happen with any two CSS classes that set the same CSS attributes. It can be as simple as:



<!DOCTYPE html>
<html>
  <head>
    <style>
      .red {
        color: red;
      }

      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <p class="blue red">Sample red text... not!</p>
  </body>
</html>


Enter fullscreen mode Exit fullscreen mode

Button with blue background
The .blue rule overrides the .red rule

As the order that the classes appear in the class attribute doesn't matter, the rule that comes later in the CSS stylesheets wins.

Coming back to Tailwind, this means that if, by coincidence, the Tailwind stylesheet file defines the .bg-blue-600 rule after the .bg-red-600, then bg-blue-600 will win every time.

The solution

Non-Tailwind

Sometimes it's possible to workaround this by changing your stylesheet and the specificity of the rules applied to the element. All of the following rules have higher priority than the original .red rule (and win over the original .blue rule):



p.red
.red.blue
#special
body .red


Enter fullscreen mode Exit fullscreen mode

There's a neat specificity calculator that's worth checking.

Tailwind

Now the solution above won't work with Tailwind, as its very concept is to have utility classes that you can use without changing any stylesheets.

When you don't know what classes may appear after your own, you need a way to detect clashes and remove all but the last occurrence. This is exactly what the tailwind-merge npm package does.

You can use it like:



import { twMerge } from "tailwind-merge";

function Button({ label, className, ...props }) {
  const classes = twMerge(`
    border
    border-black
    bg-blue-600
    p-4
    rounded-lg
    text-white
    text-xl
    ${className ?? ""}
  `);
  return <button className={classes}>{label}</button>;
}


Enter fullscreen mode Exit fullscreen mode

Button with red background
Button with the correct color

And we can verify that the rendered HTML does not contain bg-blue-600 anymore:



<button class=" border border-black p-4 rounded-lg text-white text-xl bg-red-600 ">Hello</button>

Enter fullscreen mode Exit fullscreen mode




Conclusion

Due to the fact that the order of CSS class names in the class HTML attribute does not matter, the only way to override existing classes in an element is to remove all of the previous classes that clash with the new one.

What do you think? Did you face this issue before? Do you know a better way to override the Tailwind classes that come before the new ones?

đź’– đź’Ş đź™… đźš©
diogoko
Diogo Kollross

Posted on May 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related