On tailwind class management
3 min read

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.tsx
<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.tsx
<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.tsx
<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.tsx
<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.tsx
<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.tsx
<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

utils.tsx
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!

button.tsx
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!