Skip to content

Managing Class Conflicts in UI Components

Building flexible UI components usually leads to a fight between utility classes.

2 min read
tailwind-merge
clsx
Tailwind CSS
Class Composition
CVA
Design System

Building flexible UI components usually leads to a fight between utility classes. The problem pops up when you build components with multiple variants—whether using CVA or manual logic—and then a user tries to override the default styles. A concrete example: a component has a default px-4 padding, but someone tries to pass in px-2. In the browser, the result is unpredictable because both classes are fighting for the same element.

Just concatenating strings won't fix this.

I usually use a cn helper function that combines clsx and tailwind-merge. A lot of people treat this as mandatory boilerplate, but it’s actually a composition mechanism. clsx handles building the class list based on boolean conditions, while tailwind-merge acts as the judge. It understands that px-3 and px-4 are competing for the same function, so it picks one.

One crucial thing: tailwind-merge works on a "last-wins" principle. In my component structure, the order is base classes $\rightarrow$ variants $\rightarrow$ consumer className. By putting the user's className at the very end, we give them an "emergency exit" to override any style we’ve already set.

However, we shouldn't rely on merging too much.

Being too aggressive with twMerge can actually hide design flaws. If you find yourself constantly overriding outer margins with inner padding, the problem probably isn't your merge configuration—it's the component design itself. That element might actually need to be split into two parts rather than being forced into a single node.

We also have to watch out for technical shifts, like how Tailwind v4 changes how tokens work. If you use custom tokens like bg-primary, you have to make sure tailwind-merge is configured to recognize them. Otherwise, the system will treat them as regular classes, and both will end up on the element at the same time, leading to confusing runtime results.

Whenever I see a Pull Request that tries to fix a layout issue just by slapping a new class at the end of a string, I ask myself: should this be a new variant in CVA, or are we actually fighting our own design system?

Class management should be treated as a product decision, not just a technical one. If you want button sizes to stay consistent, don't let users change the padding via className; just give them a size property. Remember, merging doesn't create rules; it only enforces the order we've already built.