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>;
}
You can use it as:
<Button label="Hello" />
And it works as advertised. Now you want to change its color to red:
<Button label="Hello" className="bg-red-600"/>
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>
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>
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
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>;
}
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>
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)
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...
@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 ?
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.
Thanks. Looking forward for more TW tips ^^
Thanks for this article. it's explains exactly what I was searching for
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?
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
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".
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.