Hello! Welcome back to our course on React.
In our last lesson, we learned how to make our UIs responsive using Tailwind's mobile-first philosophy and breakpoint prefixes like md: and lg:. This allows our components to adapt to different screen sizes.
Today, we'll take dynamic styling a step further. Instead of reacting to screen size, our styles will react to the application's data and state. Our goal is to apply styles conditionally based on component state or props. This is fundamental for creating interactive and meaningful user interfaces—think of a button that looks different when it's disabled, a form input with a red border when there's an error, or an alert box that changes color based on the message type (success, warning, or danger).
The Basics: Conditionals in the className Prop
The most straightforward way to apply a class conditionally is by using JavaScript expressions directly inside the className prop. You can use template literals with ternary operators or the logical && operator, which you're already familiar with from our previous lesson on conditional rendering.
For example, let's style a button that's active or inactive based on a prop.
functionToggleButton({ isActive }) {
return (
<button
className={`p-2 rounded-md transition-colors ${
isActive
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-800'
}`}
>
{isActive ? 'Active' : 'Inactive'}
</button>
);
}
Or using the && operator to add a class only when a condition is true:
function Alert({ message, isError }) {
return (
<div
className={`p-4 rounded-md bg-green-100 text-green-800 ${
isError && 'bg-red-100 text-red-800'
}`}
>
{message}
</div>
);
}
This Alert example has a flaw: if isError is true, both the green and red background classes are applied (bg-green-100 and bg-red-100). Which one wins? With Tailwind, the outcome can be unpredictable because it depends on the order they appear in the final generated CSS file, not the order in your className string. This leads us to a more robust approach.
The Professional's Toolkit: clsx and tailwind-merge
While template literals work for simple cases, they quickly become messy and error-prone as complexity grows. The React community has developed powerful, tiny utilities to solve this elegantly.
1. clsx: For Clean Conditional Logic
The clsx library is a popular utility for constructing className strings from a mix of strings, objects, and arrays. It makes conditional logic much cleaner.
The article 'Styling' from components.build provides a great introduction to clsx and its various syntaxes. It's a small but powerful tool.
Please read the section titled 'Conditional classes'. Focus on the different ways you can use clsx: basic conditionals, the object syntax, and arrays.
As you read, you'll see how clsx can turn our messy ToggleButton example into this:
import clsx from 'clsx';
function ToggleButton({ isActive }) {
const buttonClasses = clsx('p-2', 'rounded-md', 'transition-colors', {
'bg-blue-500 text-white': isActive,
'bg-gray-200 text-gray-800': !isActive,
});
return <button className={buttonClasses}>{/* ... */}</button>;
}
This is much more readable, but it still doesn't solve the class conflict problem we identified earlier.
2. tailwind-merge: For Resolving Class Conflicts
This is where tailwind-merge comes in. It's a function that intelligently merges Tailwind classes, automatically removing conflicts. The last class of a certain type always wins. For example, if you give it p-2 and p-4, it knows that p-4 should override p-2.
The following video explains why this is so important when building reusable components.
cn() - Every Tailwind Coder Needs It (clsx + twMerge)
The video 'cn() - Every Tailwind Coder Needs It' from ByteGrad clearly demonstrates the problem of conflicting classes and how tailwind-merge solves it.
Watch from the beginning to 03:12. Pay close attention to the example where a base bg-blue-500 conflicts with a bg-green-500 passed as a prop, and see how tailwind-merge ensures the prop style wins.
3. The cn Utility: Combining the Best of Both
The most common pattern in modern React projects is to combine these two libraries into a single helper function, often named cn (short for "class names"). This gives you both clean conditional logic from clsx and intelligent conflict resolution from tailwind-merge.
Here’s how you create it. You'd typically place this in a utility file (e.g., src/lib/utils.js):
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
First, clsx processes all the conditional logic and combines everything into a single class string. Then, twMerge takes that string, cleans it up, and resolves any conflicts.
The rest of the video you just started explains this combination perfectly.
cn() - Every Tailwind Coder Needs It (clsx + twMerge)
Now, let's see how these two libraries are combined to create the cn function.
Continue watching from 03:12 to 06:56. This section shows how to use clsx and tailwind-merge together to create the final cn utility function.
Putting It All Together: A Variant-Based Button Component
Now, let's use our new cn utility to build a flexible and reusable Button component. It will accept props like variant (primary, secondary), size (small, medium), and isDisabled to change its appearance.
This "variant" approach is a powerful pattern for conditional styling.
Mastering React with Tailwind CSS: Best Practices - GitNation
The article 'Mastering React with Tailwind CSS' provides an excellent example of a variant-based button. We will adapt its logic to use our cn function.
Review the Button.jsx code block. Notice how it defines variants and sizes as objects and then composes the final class string. The article uses a library called classNames, which is what clsx is based on.
Here's how we can implement a similar Button component using our cn function and applying best practices for ordering classes.
// src/components/Button.jsx
import { cn } from '../lib/utils'; // Adjust path as needed
function Button({
variant = 'primary',
size = 'medium',
isDisabled = false,
className, // To allow passing extra classes from parent
children,
...props
}) {
// 1. Define variant and size styles
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600',
};
const sizes = {
small: 'px-3 py-1 text-sm',
medium: 'px-4 py-2 text-base',
large: 'px-6 py-3 text-lg',
};
// 2. Compose classes with `cn`
const finalClasses = cn(
// Base styles (always applied)
'rounded-lg font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75',
// Variant styles
variants[variant],
// Size styles
sizes[size],
// Conditional styles (based on state/props)
isDisabled && 'opacity-50 cursor-not-allowed',
// User overrides (comes last to have highest priority)
className
);
return (
<button className={finalClasses} disabled={isDisabled} {...props}>
{children}
</button>
);
}
export default Button;
With this component, you can now easily create different buttons across your app:<Button>Default (Primary)</Button><Button variant="secondary" size="small">Secondary Small</Button><Button variant="danger" isDisabled={true}>Disabled Danger</Button><Button className="w-full">Full Width</Button>
A Critical Warning: Don't Construct Class Names Dynamically
There's a major "gotcha" when working with Tailwind that catches almost every new developer. Tailwind's Just-In-Time (JIT) compiler works by scanning your source files for complete, unbroken class strings. It does not execute your JavaScript to figure out what classes you might need.
This means code like this will not work:
// ❌ This will NOT work as expected
const color = 'blue';
const className = `bg-${color}-500`; // Tailwind sees this as a variable, not 'bg-blue-500'
The following video explains this limitation and shows the correct way to handle dynamic values, which reinforces the variant pattern we just used.
Avoid This Tailwind Mistake (Dynamic Classes)
This is one of the most common mistakes when starting with Tailwind. This video from ByteGrad will save you a lot of future headaches.
Please watch from the beginning to 04:42. The video explains why dynamic class construction fails and how to use an object map (like our variants object) as the correct solution.
The key takeaway is to always use full class names. Map your props or state to complete Tailwind class strings.
Correct Way (Variant Mapping):
const colorStyles = {
blue: 'bg-blue-500 text-white',
red: 'bg-red-500 text-white',
};
const MyComponent = ({ color }) => {
return <div className={cn('p-4', colorStyles[color])}>Hello</div>;
};
Conclusion
You now have a powerful and professional pattern for creating dynamic, interactive, and maintainable components with React and Tailwind CSS.
Key Takeaways:
- You can use ternary operators or
&&for very simple conditional classes, but this can become messy and lead to conflicts. - The
cnutility, which combinesclsx(for conditional logic) andtailwind-merge(for conflict resolution), is the industry-standard solution. - The "variants" pattern, where you map prop values to full class strings in an object, is a robust and scalable way to build configurable components.
- Crucially, never construct Tailwind class names with string interpolation (
bg-${color}-500). Tailwind's compiler must see the full, static class name in your code.
Next Up:
In this lesson, we created reusable styling logic inside our components. In our next lesson, we will explore another way to organize styles by learning how to organize reusable style patterns using Tailwind's @apply directive. This will allow us to create custom, reusable utility classes directly in our CSS file, which can be useful for extracting very common combinations of styles.