DEV3 Technologies home

DEV3 Developer Forum

How do I merge conflicting Tailwind classes in a component (clsx and tailwind-merge)?

dev3 ·

My Button component has px-4 py-2 bg-blue-500 and I pass className="bg-red-500 px-6" from the parent. Both classes end up on the element and the result depends on the CSS order, not on my props. How do I make the last one win?

#tailwind#tailwind-merge#clsx#react

0

1 reply

dev3 · Accepted answer

Why it breaks

In CSS, when two utilities set the same property, the one defined later in the generated stylesheet wins, not the one later in the class attribute. bg-red-500 vs bg-blue-500 is therefore unpredictable from the component's point of view.

Solution: tailwind-merge

tailwind-merge understands Tailwind's classes and removes earlier conflicting ones. Combine it with clsx for conditionals.

npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'ghost'
}

export function Button({ className, variant = 'primary', ...props }: Props) {
  return (
    <button
      className={cn(
        'rounded-md px-4 py-2 font-medium transition-colors',
        variant === 'primary' && 'bg-blue-500 text-white hover:bg-blue-600',
        variant === 'ghost' && 'bg-transparent hover:bg-gray-100',
        className, // caller overrides last
      )}
      {...props}
    />
  )
}

<Button className="bg-red-500 px-6" /> now outputs ... bg-red-500 px-6 with the blue background and px-4 removed.

Notes

  • Put the caller's className last.
  • tailwind-merge v3 matches Tailwind 4; v2 targets Tailwind 3. Use the matching major version, otherwise new v4 utilities are not recognised.
  • Custom theme tokens (such as text-brand) may need extendTailwindMerge so it knows the group.
  • For variant-heavy components, class-variance-authority pairs well with cn.

Pitfall

twMerge runs at render time and has a small cost; it is fine for components but avoid it on thousands of list rows with static classes.

0

Your reply

Every post is reviewed by DEV3 before it appears. Usually reviewed within 24 hours.

Markdown supported. Use ``` for code blocks. Images are not allowed.