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:
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:
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 clsxReal-World Use Cases
1. Accordion Component (Toggle States)
Here's a practical accordion that opens and closes:
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:
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
-
Use Objects for Toggle Logic: When you have classes that should apply based on a boolean condition, use the object syntax.
-
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)} /> -
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 } ); } -
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.