DEV Community

Diogo Kollross
Diogo Kollross

Posted on • Edited on

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?

Top comments (9)

Collapse
 
redbar0n profile image
Magne • Edited

With Tailwind, you shouldn't abstract by having components take in style props but take in semantic props so the component itself can control its own styling:

news.ycombinator.com/item?id=34352170

https://twitter.com/magnemg/status/1613505326875025410?s=20&t=HTnDLaeX5O_5WtvxYq_DEA

It's what Tailwind itself suggests, at least. And it will avoid a lot of problems: sancho.dev/blog/tailwind-and-desig...

Collapse
 
chaiyilin profile image
chaiyilin

@diogo Kollross. wondering if you try out play.tailwindcss.com/, the case red/blue you mentioned is not happening. it properly override!. i understand what you mentioned is basic css rules. just wondering any tricks by tailwind play ground or ?

Collapse
 
ivankleshnin profile image
Ivan Kleshnin • Edited

It happens. List -> Set conversion is not random in a sense of rolling a dice but it's not deterministic. Different HTML may produce a different result. It sometimes feels consistent, within a browser session or something, but it's an illusion.

Collapse
 
gabrielmlinassi profile image
Gabriel Linassi

Thanks. Looking forward for more TW tips ^^

Collapse
 
ivadyhabimana profile image
Ivad Yves HABIMANA

Thanks for this article. it's explains exactly what I was searching for

Collapse
 
uwemisrael profile image
Uwem

Thanks for this article. I want to try this out ina next-js app but I'm sacred because of the package size. 709kb, would this have an impact in production?

Collapse
 
dangkhoipro profile image
dang khoi

I think the tradeoff was worth it. After bundle, the size is 21.1kB (gzipped 6.7kB), not too big in my opinion (from bundlephobia

Collapse
 
soapproject profile image
Ivan Chou • Edited

Maybe we can solve this issue by adding an "addExclamationMark" function.
Input "bg-red-600 text-md", and then output "!bg-red-600 !text-md".

Collapse
 
mbrowne profile image
Matt Browne

Unfortunately that wouldn't work with tailwind because it can only compile classNames found in the source at build time, not dynamic strings you create at run-time like that. Perhaps if you wrote a plugin for your compiler (e.g. babel or rebuild) you could get around that issue.