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.