How to Override Tailwind CSS Styles in a React Component

How to Override Tailwind CSS Styles in a React Component

The Problem

Say you've built a <Button /> component in React that encapsulates your design system and perhaps some business logic to reuse across your React project using Tailwind CSS as the styling tool. It might look something like this:

export default function Button({ className, children, ...props }) {
  return (
    <button
      className={`py-4 px-6 text-white bg-blue-800 text-md hover:opacity-80 transition-opacity ${className}`}
      {...props}
    >
      {children}
    </button>
  );
}

Great! So using this <Button /> anywhere in your project will render the button with some default styles, like the bg-blue-800 background colour:

https://codesandbox.io/p/devbox/tailwind-override-in-react-component-disabled-s3jvt3

Notice, that we also support passing the className prop to this button component, for when there's an exceptional use-case where we'd want to tweak some button styles on an ad-hoc basis.

Consider the case, where we want to change the blue background colour to black. The bg-black Tailwind utility should help us do that. Wonderful, let's see what happens if we pass this in the className prop for the <Button /> component:

<Button className="bg-black">Styled button</Button>

And here's what the result looks like:

That's strange! The button still remains blue. Why didn't the bg-black class name override bg-blue-800?! After all, if we look at how the className prop is used on our <Button /> component, it's specified at the end of all the default Tailwind utilities:

className={`py-4 px-6 text-white bg-blue-800 text-md hover:opacity-80 transition-opacity ${className}`}

so the rendered HTML looks like this:

The first thought here is that since bg-black is used after bg-blue-800, it should take precedence.

However, this is not true. The order in which the class names appear in the class attribute of an HTML element does NOT matter. What matters is the order of these class rules defined in the cascading stylesheet instead.

In our case, Tailwind CSS includes the rules for all the utilities we enjoy using in our app when we include its stylesheets upon installing it. In our example the index.css file does this with:

@tailwind base;
@tailwind components;
@tailwind utilities;

So, now we know that in order for bg-black to take precedence over bg-blue-800, we need that style rule to appear later in the stylesheet, but we don't have control over it! How do we go around this?

The Solution

So, how do we override Tailwind CSS Styles in a React Component?

We can use a package called tailwind-merge! This package exposes a twMerge helper that can help remove conflicting classes and prefer the className that you pass to a React component instead. Let's see how we can make use of this helper:

import { twMerge } from "tailwind-merge";

export default function Button({ className, children, ...props }) {
  return (
    <button
      className={twMerge(
        "py-4 px-6 text-white bg-blue-800 text-md hover:opacity-90 transition-opacity",
        className,
      )}
      {...props}
    >
      {children}
    </button>
  );
}

We've updated our <Button /> component so the className is now the return value of the twMerge function imported from "tailwind-merge", with the default button classes as the first argument and the provided className prop as the second argument. With this in place, using our <Button /> with the className="bg-black" prop now renders the button as intended:

https://codesandbox.io/p/devbox/tailwind-override-in-react-component-tailwind-merge-d8nj7p?file=%2Fsrc%2FButton.component.tsx&embed=1

Bonus

If you use the clsx package for constructing classNames in React conditionally, a new helper function can help you combine clsx and twMerge and use it frequently across React components. Here's such a helper function called cn in TypeScript:

import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputClasses: ClassValue[]) {
  return twMerge(clsx(inputClasses))
}

So you can make use of this in the <Button /> component like so,

import { cn } from "../cn.helper";

export default function Button({ className, children, ...props }) {
  return (
    <button
      className={cn(
        "py-4 px-6 text-white bg-blue-800 text-md hover:opacity-90 transition-opacity",
        className,
      )}
      {...props}
    >
      {children}
    </button>
  );
}

Thanks for reading!