VAI logo
← Back to Blog

What is clsx and why is it in every TypeScript project?

July 27, 2025

You've probably seen it in package.json files everywhere, but never really understood what it does.

It's called clsx (pronounced "class-ex").

And yes, it's in practically every modern React/TypeScript project.

So what exactly is it, and why should you care?

What is clsx?

clsx is a tiny (only 239 bytes!) utility that helps you conditionally join CSS class names together. Think of it as a smart string concatenator specifically designed for the className prop in React.

Why do we need it?

Let's start with a real problem. Imagine you're building a button component that:

  • Has a base style
  • Changes color based on variant (primary, secondary, danger)
  • Can be disabled
  • Has different sizes (small, medium, large)

The Problem: Without clsx

Here's what most beginners write:

❌ Without clsx - Verbose and Error-Prone
function Button({ variant, size, disabled, children }) {
  let className = "btn";
  
  if (variant === "primary") {
    className += " btn-primary";
  } else if (variant === "secondary") {
    className += " btn-secondary";
  } else if (variant === "danger") {
    className += " btn-danger";
  }
  
  if (size === "small") {
    className += " btn-sm";
  } else if (size === "large") {
    className += " btn-lg";
  }
  
  if (disabled) {
    className += " btn-disabled opacity-50 cursor-not-allowed";
  }
  
  return <button className={className}>{children}</button>;
}

This works, but it's verbose and error-prone. You might forget spaces, duplicate classes, or end up with trailing spaces.

The Solution: With clsx

Here's the same component with clsx:

✅ With clsx - Clean and Declarative
import clsx from 'clsx';

function Button({ variant, size, disabled, children }) {
  return (
    <button 
      className={clsx(
        'btn',
        {
          'btn-primary': variant === 'primary',
          'btn-secondary': variant === 'secondary',
          'btn-danger': variant === 'danger',
          'btn-sm': size === 'small',
          'btn-lg': size === 'large',
          'btn-disabled opacity-50 cursor-not-allowed': disabled
        }
      )}
    >
      {children}
    </button>
  );
}

Much cleaner, right? But clsx can do even more!

Installation

npm install clsx

Real-World Use Cases

1. Accordion Component (Toggle States)

Here's a practical accordion that opens and closes:

Accordion Component with State-Based Styling
import { useState } from 'react';
import clsx from 'clsx';

function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="accordion">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className={clsx(
          'accordion-header',
          'flex justify-between items-center p-4',
          {
            'bg-blue-50': isOpen,
            'hover:bg-gray-50': !isOpen
          }
        )}
      >
        {title}
        <span className={clsx(
          'transform transition-transform',
          { 'rotate-180': isOpen }
        )}>

        </span>
      </button>
      
      <div className={clsx(
        'accordion-content',
        'overflow-hidden transition-all duration-300',
        {
          'max-h-96 p-4': isOpen,
          'max-h-0': !isOpen
        }
      )}>
        {children}
      </div>
    </div>
  );
}

2. Form Input with Validation States

Perfect for showing error, success, or warning states:

Form Input with Validation States
function Input({ error, success, warning, ...props }) {
  return (
    <input
      className={clsx(
        'px-4 py-2 border rounded-lg transition-colors',
        'focus:outline-hidden focus:ring-2',
        {
          'border-red-500 focus:ring-red-200': error,
          'border-green-500 focus:ring-green-200': success,
          'border-yellow-500 focus:ring-yellow-200': warning,
          'border-gray-300 focus:ring-blue-200': !error && !success && !warning
        }
      )}
      {...props}
    />
  );
}

3. Navigation Menu with Active States

Common pattern for highlighting the current page:

import { useRouter } from 'next/router';
import clsx from 'clsx';

function NavLink({ href, children }) {
  const router = useRouter();
  const isActive = router.pathname === href;
  
  return (
    <a
      href={href}
      className={clsx(
        'px-3 py-2 rounded-md text-sm font-medium transition-colors',
        {
          'bg-gray-900 text-white': isActive,
          'text-gray-700 hover:bg-gray-100': !isActive
        }
      )}
    >
      {children}
    </a>
  );
}

4. Card Component with Multiple Variants

function Card({ variant = 'default', elevated, children }) {
  return (
    <div className={clsx(
      'rounded-lg p-6',
      {
        // Variant styles
        'bg-white border border-gray-200': variant === 'default',
        'bg-blue-50 border border-blue-200': variant === 'info',
        'bg-red-50 border border-red-200': variant === 'error',
        'bg-green-50 border border-green-200': variant === 'success',
        
        // Elevation
        'shadow-lg': elevated,
        'shadow-xs': !elevated
      }
    )}>
      {children}
    </div>
  );
}

Advanced Features

Accepting Multiple Argument Types

clsx is flexible and accepts various input types:

// Strings
clsx('foo', 'bar'); // 'foo bar'

// Objects
clsx({ foo: true, bar: false }); // 'foo'

// Arrays
clsx(['foo', 'bar']); // 'foo bar'

// Mixed - this is where clsx shines!
clsx(
  'base-class',
  { 'conditional-class': someCondition },
  ['array', 'of', 'classes'],
  someCondition && 'another-class'
); 

Handling Falsy Values

clsx automatically filters out falsy values:

clsx('foo', null, undefined, false, 0, '', 'bar'); 
// Result: 'foo bar'

This means you can write cleaner conditional logic:

clsx(
  'button',
  isPrimary && 'button-primary',
  isLarge && 'button-large',
  isLoading && 'opacity-50 cursor-wait'
);

Working with Tailwind CSS

clsx pairs perfectly with Tailwind CSS for dynamic styling:

function Alert({ type, children }) {
  return (
    <div className={clsx(
      'p-4 rounded-lg flex items-start gap-3',
      {
        'bg-blue-100 text-blue-900': type === 'info',
        'bg-yellow-100 text-yellow-900': type === 'warning',
        'bg-red-100 text-red-900': type === 'error',
        'bg-green-100 text-green-900': type === 'success'
      }
    )}>
      {children}
    </div>
  );
}

Pro Tips

  1. Use Objects for Toggle Logic: When you have classes that should apply based on a boolean condition, use the object syntax.

  2. Combine with CSS Modules: clsx works great with CSS Modules:

    import styles from './Button.module.css';
    import clsx from 'clsx';
    
    <button className={clsx(styles.button, styles.primary)} />
  3. Extract Complex Logic: For components with many conditional classes, consider extracting the logic:

    function getButtonClasses({ variant, size, disabled }) {
      return clsx(
        'btn',
        `btn-${variant}`,
        `btn-${size}`,
        {
          'opacity-50 cursor-not-allowed': disabled
        }
      );
    }
  4. Performance: There's also clsx/lite (only 140 bytes) if you only need string concatenation without object/array support.

Why Not Just Use Template Literals?

You might wonder why not just use template literals:

className={`btn ${variant} ${size} ${disabled ? 'disabled' : ''}`}

The problems:

  • You might end up with extra spaces
  • Empty strings still add spaces
  • More verbose for multiple conditions
  • Harder to read with complex logic

Conclusion

clsx solves a real problem in modern React development. It makes conditional class names:

  • Cleaner to write
  • Easier to read
  • Less error-prone
  • More performant

Next time you find yourself concatenating class names with template literals or complex string manipulation, reach for clsx. Your future self (and your teammates) will thank you!

Remember: Good code isn't just about making it work—it's about making it maintainable and readable. clsx helps you achieve both.