Hussaini Marsidi
Main page
02 January 20266 minutes read

Criminally Underrated Duo: CLSX & TW Merge

I've been developing web applications for half a decade now, and in the last few years my go to tool for styling is Tailwind CSS. I've been using it for everything from small side projects to large-scale enterprise applications. It's a powerful tool that allows you to style your application quickly and efficiently.

To be fair, during the time of writing this article many companies have applied Tailwind CSS to their projects, but most of them are not using it to its full potential. They are not using the power of CLSX and TW Merge to its full potential. In a glance, this utility seemed like an unnecessary layer to the stack, "You can avoid this library by writing the classes manually". Fair argument but let me tell you why this is a bad idea.

What is CLSX?

When I first saw this library, I was like why we would need this library? On the surface it seemed like training wheels for tailwind classes. But that is ar from the truth. Let me give you an example.

CLSX is a tiny utility for constructing className strings conditionally. The real power comes when you need to conditionally apply classes based on props, state, or other conditions.

Without CLSX - This is how you'd typically handle multiple variants:

const Button = ({ variant = 'primary', size = 'md', isDisabled, children }) => {
  let className = 'font-medium rounded-md transition-all'
  
  // Variant styles
  if (variant === 'primary') {
    className += ' bg-blue-600 text-white hover:bg-blue-700'
  } else if (variant === 'secondary') {
    className += ' bg-gray-200 text-gray-900 hover:bg-gray-300'
  } else if (variant === 'tertiary') {
    className += ' bg-transparent text-blue-600 border border-blue-600 hover:bg-blue-50'
  } else if (variant === 'danger') {
    className += ' bg-red-600 text-white hover:bg-red-700'
  }
  
  // Size styles
  if (size === 'sm') {
    className += ' px-3 py-1.5 text-sm'
  } else if (size === 'md') {
    className += ' px-4 py-2 text-base'
  } else if (size === 'lg') {
    className += ' px-6 py-3 text-lg'
  }
  
  // Disabled state
  if (isDisabled) {
    className += ' opacity-50 cursor-not-allowed'
  }
  
  return <button className={className} disabled={isDisabled}>{children}</button>
}

This approach quickly becomes messy with nested conditionals, string concatenation, potential spacing issues, and hard-to-maintain logic as you add more variants.

With CLSX - Clean, readable, and maintainable:

import { clsx } from 'clsx'

const Button = ({ variant = 'primary', size = 'md', isDisabled, children }) => {
  const className = clsx(
    'font-medium rounded-md transition-all',
    {
      // Variant styles
      'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
      'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
      'bg-transparent text-blue-600 border border-blue-600 hover:bg-blue-50': variant === 'tertiary',
      'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
      // Size styles
      'px-3 py-1.5 text-sm': size === 'sm',
      'px-4 py-2 text-base': size === 'md',
      'px-6 py-3 text-lg': size === 'lg',
      // Disabled state
      'opacity-50 cursor-not-allowed': isDisabled
    }
  )
  
  return <button className={className} disabled={isDisabled}>{children}</button>
}

CLSX handles spacing automatically, makes conditional logic explicit, and prevents common bugs like missing spaces or duplicate classes. It's especially powerful when combined with component props and state management.

What is TW Merge?

TW Merge is a utility that intelligently merges Tailwind CSS classes and resolves conflicts. Unlike simple string concatenation, it understands Tailwind's class hierarchy and automatically removes conflicting classes, keeping only the last one. This is crucial when you have base classes that might conflict with variant-specific classes.

Without TW Merge - Conflicting classes cause unexpected behavior:

const Card = ({ className, variant = 'default' }) => {
  const baseClasses = 'p-4 rounded-lg bg-white text-gray-900'
  
  let variantClasses = ''
  if (variant === 'primary') {
    variantClasses = 'bg-blue-500 text-white' // Conflicts with bg-white and text-gray-900!
  } else if (variant === 'danger') {
    variantClasses = 'bg-red-500 text-white p-6' // Conflicts with p-4!
  }
  
  // Both conflicting classes end up in the final string
  const finalClassName = `${baseClasses} ${variantClasses} ${className || ''}`
  
  return <div className={finalClassName}>Card content</div>
}

// Result: 'p-4 rounded-lg bg-white text-gray-900 bg-blue-500 text-white'
// Both bg-white AND bg-blue-500 are applied - CSS specificity determines the winner
// This leads to unpredictable styling!

The problem is that both bg-white and bg-blue-500 are in the className string. While CSS will apply the last one in the stylesheet, this creates unpredictable behavior and bloated HTML.

With TW Merge - Conflicts are automatically resolved:

import { twMerge } from 'tailwind-merge'

const Card = ({ className, variant = 'default' }) => {
  const baseClasses = 'p-4 rounded-lg bg-white text-gray-900'
  
  let variantClasses = ''
  if (variant === 'primary') {
    variantClasses = 'bg-blue-500 text-white' // Replaces bg-white and text-gray-900
  } else if (variant === 'danger') {
    variantClasses = 'bg-red-500 text-white p-6' // Replaces p-4
  }
  
  // TW Merge intelligently resolves conflicts
  const finalClassName = twMerge(baseClasses, variantClasses, className)
  
  return <div className={finalClassName}>Card content</div>
}

// Result: 'rounded-lg bg-blue-500 text-white' (for primary variant)
// TW Merge removed bg-white and text-gray-900, keeping bg-blue-500 and text-white
// Clean, predictable, and exactly what you intended!

TW Merge understands that bg-blue-500 conflicts with bg-white, text-white conflicts with text-gray-900, and p-6 conflicts with p-4. It automatically removes the conflicting classes, ensuring your variant styles work as expected. This is especially powerful when combining base component classes with props, variants, and user-provided className overrides. t’s like wonderful guardrails.

How to use them together?

I enjoy taking some time to read how open source usually works under the hood. During one time I had the frustration related to how we structure tailwind in one of the project that I was working on, I decided to look around implementations in open source and stumbled upon the Shadcn/UI. It's a popular library that is used by many companies and it's a great example of how to use CLSX and TW Merge together, if you are interested in how it works, you can check out the source code for more details.

The magic happens when you combine them. The typical pattern is to use clsx to conditionally build your class string, then pass it through twMerge to resolve any conflicts. Here's how you'd typically create a utility function:

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

// Utility function that combines both
function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Now let's see how this helps in a real-world scenario:

Without CLSX + TW Merge - A nightmare of conditionals and conflicts:

const Button = ({ 
  variant = 'primary', 
  size = 'md', 
  isDisabled, 
  className,
  children 
}) => {
  let baseClasses = 'font-medium rounded-md transition-all'
  
  // Variant logic
  if (variant === 'primary') {
    baseClasses += ' bg-blue-600 text-white hover:bg-blue-700'
  } else if (variant === 'secondary') {
    baseClasses += ' bg-gray-200 text-gray-900 hover:bg-gray-300'
  }
  
  // Size logic
  if (size === 'sm') {
    baseClasses += ' px-3 py-1.5 text-sm'
  } else if (size === 'md') {
    baseClasses += ' px-4 py-2 text-base'
  }
  
  // Disabled state
  if (isDisabled) {
    baseClasses += ' opacity-50 cursor-not-allowed'
  }
  
  // User override - but what if they pass conflicting classes?
  const finalClassName = `${baseClasses} ${className || ''}`
  
  return <button className={finalClassName} disabled={isDisabled}>
    {children}
  </button>
}

// Usage:
<Button variant="primary" className="bg-red-500 p-8">
  Click me
</Button>
// Result: '... bg-blue-600 ... bg-red-500 ... px-4 py-2 ... p-8'
// Both bg-blue-600 AND bg-red-500 are present - unpredictable!
// Both px-4 py-2 AND p-8 are present - which one wins?!

With CLSX + TW Merge - Clean, predictable, and flexible:

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

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

const Button = ({ 
  variant = 'primary', 
  size = 'md', 
  isDisabled, 
  className,
  children 
}) => {
  const buttonClassName = cn(
    // Base classes
    'font-medium rounded-md transition-all',
    // Variant styles
    {
      'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
      'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
      'bg-transparent text-blue-600 border border-blue-600': variant === 'tertiary',
    },
    // Size styles
    {
      'px-3 py-1.5 text-sm': size === 'sm',
      'px-4 py-2 text-base': size === 'md',
      'px-6 py-3 text-lg': size === 'lg',
    },
    // Disabled state
    {
      'opacity-50 cursor-not-allowed': isDisabled
    },
    // User override - TW Merge will resolve conflicts!
    className
  )
  
  return <button className={buttonClassName} disabled={isDisabled}>
    {children}
  </button>
}

// Usage:
<Button variant="primary" className="bg-red-500 p-8">
  Click me
</Button>
// Result: 'font-medium rounded-md transition-all text-white hover:bg-blue-700 bg-red-500 p-8'
// TW Merge removed bg-blue-600 (conflicts with bg-red-500)
// TW Merge removed px-4 py-2 (conflicts with p-8)
// Clean, predictable, and user override works perfectly!

Right off the bat, you can see how much cleaner and more readable the code is. You can see the conditional logic is explicit and you can see the conflicts are resolved automatically. You can also see the user override is working perfectly. ESLint also will help you to catch any potential conflicts or missing classes.

Conclusion

I believe these two utilities are underrated and they are a great tool to have in your toolbox. I was stubborn about not using them in my projects and only came back to use them again, and they lifted the team’s productivity significantly. Your project can benefit from this as well, give it a try and see the difference for yourself. Peace.


© 2026 Hussaini Marsidi. Made in Malaysia 🇲🇾.