Optimizing Tailwind CSS in Next.js: Efficient Dynamic Class Management

Discover how to streamline your component styling with advanced techniques for combining utility classes and managing conditional styles

May 3, 2024

By Craig Hicks

We all love Tailwind CSS. That utility-first CSS framework that has gained significant traction in web development for its flexible, highly customizable approach to styling applications. By offering a vast array of utility classes, Tailwind enables direct in-markup design customization, fostering rapid prototyping and streamlined workflows. This leads to smaller CSS files and a more integrated development experience, avoiding the frequent back-and-forth between HTML and CSS files.

However, dynamically building classes can produce unwanted gotchas. Mismanagement can result in increased bundle sizes, which affects application performance. Additionally, class conflicts may occur, leading to styles not being applied as intended. This technique can also complicate the codebase, making it challenging to manage conditional classes directly within components. Here is where utilizing efficient tools like clsx and tailwind-merge can be of great benefit.

The Code

Let’s create a simple button component that takes in a couple parameters. A primary boolean to highlight our call to action and a disabled state.

type ButtonProps = {
  primary: boolean;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({ primary, disabled }) => {
  let buttonClass = 'px-4 py-2 rounded transition-colors ';

  if (primary) {
    buttonClass += 'bg-blue-500 hover:bg-blue-600 text-white';
  } else if (!primary && !disabled) {
    buttonClass += 'bg-gray-300 hover:bg-gray-400 text-gray-600';
  }

  if (disabled) {
    buttonClass = 'px-4 py-2 rounded transition-colors bg-gray-200 text-gray-400 pointer-events-none';
  }

  return <button className={buttonClass} disabled={disabled}>Click me</button>;
};

This works but using string concatenation for class names like this can quickly become cumbersome and error-prone as the complexity of your component grows. We have some redundant code too as we don’t want to have class conflicts if the button is primary and disabled.

This is where a tool like clsx can help (the classnames library is another popular option). clsx is a utility library used for conditionally joining class names together in a more manageable and readable way. It enables developers to dynamically construct class strings based on the truthiness of conditions. Here is how we can update our component using that library. Let’s use it to clean up our conditional statements.

import clsx from 'clsx';

type ButtonProps = {
  primary: boolean;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({ primary, disabled }) => {
  const buttonClass = clsx(
    'px-4 py-2 rounded transition-colors',
    {
      'bg-blue-500 hover:bg-blue-600 text-white': primary,
      'bg-gray-300 hover:bg-gray-400 text-gray-600': !primary && !disabled,
      'bg-gray-200 text-gray-400 pointer-events-none': disabled
    }
  );

  return <button className={buttonClass} disabled={disabled}>Click me</button>;
};

This approach alone allows us to simplify the conditions and readability of the code. There is a bit of an issue still though. The library is conditionally concatenating the class strings for us but there is a scenario where we are going to have unwanted results. If both primary and disabled are set to true, we are going to receive an output like this:

px-4 py-2 rounded transition-colors bg-blue-500 hover:bg-blue-600 text-white bg-gray-200 text-gray-400 pointer-events-none

Because we are just concatenating the strings, we are getting conflicting background and text color styles in our class string. And though it will preform as expected since the CSS will be built in the order of the classes, they will all be added causing unnecessary bloat. This can be solved by adding && !disabled to the truthy primary condition. Fine, problem solved for this instance but there is a better solution.

Enter tailwind-merge. It is a utility designed to efficiently merge Tailwind CSS class names, automatically handling conflicts between overlapping styles and ensuring the most specific styles prevail. This will simplify the management of dynamic class combinations, making it easier to apply conditional styling without redundancy or potential errors.

Let’s integrate it into our code by passing our buttonClasses variable to it.

import clsx from 'clsx';
import twMerge from 'tailwind-merge';

type ButtonProps = {
  primary: boolean;
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ primary, disabled }) => {
  const buttonClass = clsx(
    'px-4 py-2 rounded transition-colors',
    {
      'bg-blue-500 hover:bg-blue-600 text-white': primary,
      'bg-gray-300 text-gray-600': !primary && !disabled,
      'bg-gray-200 text-gray-400 pointer-events-none': disabled
    }
  );

  return <button className={**twMerge**(buttonClass)} disabled={disabled}>Click me</button>;
};

Now when both primary and disabled are true, tailwind-merge will trim out the bg-blue-500 and text-white that are being overwritten by the disabled classes.

In many situations, we would want this component used in many situations where the developer may need to pass in styles to satisfy their particular use case. This is where the libraries really shine.

Let’s update our component to accept styles passed in.

import clsx from 'clsx';
import twMerge from 'tailwind-merge';

type ButtonProps = {
    primary: boolean;
    disabled?: boolean;
} **& React.HTMLAttributes<HTMLElement>**;

const ButtonCLSXTWM: React.FC<ButtonProps> = ({
    primary,
    disabled,
    **...props**
}) => {
    const buttonClass = clsx(
        'px-4 py-2 rounded transition-colors',
        {
            'bg-blue-500 hover:bg-blue-600 text-white': primary,
            'bg-gray-300 hover:bg-gray-400 text-gray-600':
                !primary && !disabled,
            'bg-gray-200 text-gray-400 pointer-events-none': disabled,
        },
        **props.className**
    );

    return (
        <button className={twMerge(buttonClass)} disabled={disabled}>
            Click me
        </button>
    );
};

Now developers can override colors, padding or roundness and they will override the pre-defined classes cleanly.

Let’s make the button red.

<Button primary className="bg-red-500 hover:bg-red-600" />

Tying Up Loose Ends on Optimizing Dynamic Class Management

This solution would be best suited as a helper utility to be added into components. Lets see what that would look like.

💡 Make it a utility function to be used throughout the app
// utils.ts

import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// Button.tsx

import { cn } from 'utils';

type ButtonProps = {
    primary: boolean;
    disabled?: boolean;
} & React.HTMLAttributes<HTMLElement>;

export const ButtonCLSXTWM: React.FC<ButtonProps> = ({
    primary,
    disabled,
    ...props
}) => {
    const buttonClass = cn(
        'px-4 py-2 rounded transition-colors',
        {
            'bg-blue-500 hover:bg-blue-600 text-white': primary,
            'bg-gray-300 hover:bg-gray-400 text-gray-600':
                !primary && !disabled,
            'bg-gray-200 text-gray-400 pointer-events-none': disabled,
        },
        props.className
    );

    return (
        <button className={buttonClass} disabled={disabled}>
            Click me
        </button>
    );
};

// Header.tsx

import { cn } from './utils';

type HeaderProps = {
    title: string;
} & React.HTMLAttributes<HTMLElement>;

export const Header: React.FC<HeaderProps> = ({ title, className }) => {
    return (
        <header>
            <h1
                className={cn(
                    'text-xl font-semibold bg-gray-100 text-gray-800',
                    className
                )}
            >
                {title}
            </h1>
        </header>
    );
};

And just like that we get the benefits of the libraries!

I prefer to keep the function name small like cn (class name) so that it can do it’s thing and the developer can focus on the code that matters for the component.



A photo of Craig Hicks, an employee at Fishtank

Craig Hicks

Front End Developer

Craig Hicks (or ‘chicks’ for short) is a seasoned developer whose expertise spans web development, digital media, project management, and leadership. Throughout his career he has evolved from hands-on coding roles to strategic management positions, and aims to apply his experience into his passion for problem-solving and development. His love for continuous learning and diving into new challenges applies to both his professional life and personal pursuits. Outside of work, he enjoys music, movies, and sports with friends and family.