Sometimes managing tailwind classes for simple components can be a bit cumbersome and sometimes downright awkward and difficult to manage due to its length or complexity.
Consider a simple button
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25">
Button
</button>
Now let’s add hover classes
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25 hover:bg-black/75 hover:text-white hover:border-white/40">
Button
</button>
We should really add focus classes for accessibility
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25 hover:bg-black/75 hover:text-white hover:border-white/40 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500">
Button
</button>
Hmm, we need disabled styles too
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25 hover:bg-black/75 hover:text-white hover:border-white/40 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 disabled:bg-slate-50 disabled:text-slate-500 disabled:border-slate-200 disabled:shadow-none">
Button
</button>
Uhm, what about dark mode
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25 dark:bg-white dark:text-black/75 dark:border-black/25 hover:bg-black/75 hover:text-white hover:border-white/40 hover:dark:bg-white/75 hover:dark:text-black hover:dark:border-black/40 focus:outline-none focus:border-purple-600 focus:ring-1 focus:ring-purple-600 dark:focus:border-purple-400 dark:focus:ring-1 dark:focus:ring-purple-400 disabled:bg-black/50 disabled:text-white/50 disabled:border-white/15 dark:disabled:bg-white/50 dark:disabled:text-black/50 dark:disabled:border-black/15 dark:disabled:shadow-none">
Button
</button>
Lastly, we want smooth transitions on hover
<button className="flex items-center justify-center text-sm px-2 py-1 border rounded-md bg-black text-white/75 border-white/25 dark:bg-white dark:text-black/75 dark:border-black/25 hover:bg-black/75 hover:text-white hover:border-white/40 hover:dark:bg-white/75 hover:dark:text-black hover:dark:border-black/40 focus:outline-none focus:border-purple-600 focus:ring-1 focus:ring-purple-600 dark:focus:border-purple-400 dark:focus:ring-1 dark:focus:ring-purple-400 disabled:bg-black/50 disabled:text-white/50 disabled:border-white/15 dark:disabled:bg-white/50 dark:disabled:text-black/50 dark:disabled:border-black/15 dark:disabled:shadow-none transition-colors duration-300 ease-in-out">
Button
</button>
Our button is now a never-ending horizontal scroll of classes that only Mike Ross could read.
Enter clsx and tailwind-merge
I use clsx and tailwind-merge in every project that uses Tailwind.
twMerge
is a utility that merges Tailwind CSS class names while resolving conflicts. It ensures that only the last conflicting class in the list is applied, which is particularly useful when dynamically combining Tailwind classes.
clsx
is a utility for constructing className strings conditionally. It allows you to combine classes based on various conditions and is often used for its simplicity and ease of use.
Step 1: Install
pnpm install clsx tailwind-merge
Step 2: Create a helper function
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
//export the helper function
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Step 2: Use it!
import { cn } from "../lib/utils";
export default function Button({text, rounded, size}: {text: string, rounded?: boolean, size?: "sm"}) {
return (
<button
className={cn(
//base classes
"flex items-center justify-center text-sm px-2 py-1 border transition-colors duration-300 ease-in-out",
//default button classes
"bg-black text-white/75 border-white/25 dark:bg-white dark:text-black/75 dark:border-black/25",
//hover styles
"hover:bg-black/75 hover:text-white hover:border-white/40 hover:dark:bg-white/75 hover:dark:text-black hover:dark:border-black/40",
//focus styles
"focus:outline-none focus:border-purple-600 focus:ring-1 focus:ring-purple-600 dark:focus:border-purple-400 dark:focus:ring-1 dark:focus:ring-purple-400",
//disabled styles
"disabled:bg-black/50 disabled:text-white/50 disabled:border-white/15 dark:disabled:bg-white/50 dark:disabled:text-black/50 dark:disabled:border-black/15 dark:disabled:shadow-none",
//some conditional style
rounded && "rounded-md",
//another conditional style
size === "sm" ? "text-sm" : "text-base"
)}
>
{text}
</button>
);
)
}
Definately, much more readable and maintainable albeit a bit more verbose.
You can compose your classes with cn
any way you like, but I find it most intuitive to group them by their purpose.
Give it a try in your next tailwind project!