Utility functionMerging CSS classes

Elements in a web app often need to receive CSS classes in multiple ways at once: statically, conditionally, and passed as component prop. This post shows how to merge these CSS classes using a simple utility function; as an alternative to using a third-party package.

When using Tailwind CSS or other CSS frameworks we need to put a lot of CSS classes on elements. Often there are static classes, as well as conditional classes, and classes passed through from parent components as props. How can we combine these to a single class string to be set as the value of the element's class attribute (respectively the className prop in React)?

Packages like classnames or clsx provide a utility function that makes this easy:

function DropdownMenu({
    …
    className,
    size = DropdownMenuSize.NORMAL,
}: PropsWithChildren<DropdownMenuProps>) {
    return (
        …
            <div
                …
                className={classNames(
                    className, // classes passed as component prop
                    "z-20 py-1 absolute rounded-md …", // static classes
                    { // dynamic classes
                        "w-56": size === DropdownMenuSize.NORMAL,
                        "w-96": size === DropdownMenuSize.LARGE,
                    }
                )}
            >
                …
            </div>
        …
    );
}

The function (here classNames from the classnames package) takes a dynamic number of arguments and generates the final class string as the return value.

Since adding an additional dependency to a project might not be worth the cost for such trivial functionality, I recently decided to go with my own version of the function; inspired by this blog post.

In the following, I'll go through the thought process of why having such a utility function makes sense, what requirements we have for it, and how we can build our own implementation.

Note: Although some of the code samples shown in this post are specific to React, the utility function itself can be used with any web framework. React has a className prop to control the HTML class attribute. The value of this prop is passed through to the DOM tree as-is. So, if you are not into React, just imagine setting the class attribute in a way you are familiar with.

Coping without a function

Let's start with the simplest case. If we only have static classes on an element, we just separate them by spaces and set them as a string literal:

<div className="class-a class-b class-c">…</div>

For adding conditional classes, we could use a template string with a ternary operator:

<div className={`class-a ${condition ? "class-b class-c" : "class-d"}`}>…</div>

What do we do if we only want to have an additional class-b in one of the cases, but no additional class in the other? We can use an empty string:

<div className={`class-a ${condition ? "class-b" : ""}`}>…</div>

Using undefined or null instead of an empty string would not work, as it would render this literally into the class string.

Another requirement is to add classes that have been passed via a prop (e.g. className) to the component. We could just concatenate these at the end:

<div className={`class-a … ${className || ""}`}>…</div>

We added an empty string as a fallback so that className can be an optional prop. This is important because without the fallback we would—as above—end up with a literal undefined string in our class string if className is not passed.

The template string approach works fine for such simple cases. But for more complex sets of conditions and longer lists of classes, it gets hard to read and maintain. Also, the ternary operator is not the most flexible tool. Although we don't want any additional classes in some cases, we have to provide a value for both cases of the ternary operator. For the template strings, this has to be an empty string, which is an unintuitive pattern.

Designing the API

To overcome the problems of using a template string, we define a utility function cn (for “class names”) for creating the final string. For now, let's just look at evolving the function signature. We will cover the implementation for the full functionality later in this post.

Our function takes a dynamic number of string arguments using a rest parameter and returns the class string:

function cn(...args: string[]): string { … }

This makes combining the differently provided classes much nicer, as we can now put them easily into multiple lines and don't have all the template string boilerplate.

<div
  className={cn(
    "class-a … ",
    condition ? "class-b class-c" : "class-d",
    className
  )}
>
  …
</div>

For more complex sets of conditions, the object pattern has become popular, as it is offered for example by the classnames package. Here we define an object, in which each property is a conditional class string. The property name defines the classes and depending on whether the property's value is truthy or falsy, the class string is applied or not:

{
  "class-a class-b": condition1,
  "class-c": condition2,
  "class-d": variableA === "value X",
}

This may seem counterintuitive as the typical order of condition and result value is inverted: First, we have the value to be applied if the condition is met and only then the condition. I prefer this however over using nested ternary operators, especially since the latter does not behave nicely when using a code formatter like Prettier.

An alternative condition pattern is to leverage the short-circuit behavior of the logical AND and OR operators in JavaScript. If we only want to apply classes if the condition is met, but none if not, we can express it this way:

condition && "class-a class-b"

JavaScript evaluates the logical AND from left to right. If the left-hand side is falsy it short-circuits and does not evaluate the right-hand side. The return value of the logical AND is the last evaluated operand. Thus if condition is falsy, we just get the falsy value; if it is truthy we get the class string.

Likewise, for applying classes if a condition is not met, we can use the logical OR operator which has the opposite behavior:

condition || "class-c class-d"

Here we need to make sure that condition is a boolean. If it is true (strictly, not just truthy) and thus short-circuits, our utility function will be able to filter this out. If it was a different type of truthy value like an object or string it would get interpreted as a class string, which we don't want in this case. For the logical AND above this is not an issue, since a falsy value can never be a valid class string and can thus safely be filtered out.

Leveraging this short-circuiting is quite common in JavaScript, especially the logical OR is used a lot for fallback values (as we already did above; this usage is similar to the nullish coalescing operator ??). For this reason, I like using the logical AND and OR operators for applying conditional classes, at least in my personal projects. They result in more compact code as opposed to ternary operators. And when applying only a single condition I also prefer them over the object syntax shown above as it is less verbose, especially in the case of the logical OR (in the object pattern an extra negation would be needed, increasing the cognitive load).

I want to note though, that this practice generally is debatable, for instance as advocated against by Kent C. Dodds here, in the context of JSX. At least the readability argument also applies to our usage of the logical AND and OR operators for conditional classes. And as noted above, when using the logical OR pattern we need to be extra careful to have a boolean condition. In a development team, the usage of such patterns should definitely be discussed and agreed upon in the development guidelines of the team.

With these patterns in place, let's now extend the signature of our utility function:

function cn(...args: ClassNameConfig[]): string { … }

type ClassNameConfig = string | object | undefined | null | boolean;

This allows us to use all the described patterns above:

<div
  className={cn(
    "class-a … ", // string: static classes
    condition1 ? "class-b class-c" : "class-d", // string: ternary operators
    {
      // object: object pattern
      "class-e": condition2,
      "class-f": condition3,
    },
    condition4 && "class-g", // undefined | null | false | string: logical AND pattern
    condition5 || "class-h", // true | string: logical OR pattern
    className // undefined | string: (optional) className props
  )}
>
  …
</div>

This is just to illustrate all the possible usages. Probably it doesn't make much sense to mix some of the patterns, like the logical AND pattern together with the object pattern.

Implementing the utility function

The implementation of the cn function could look like this:

function cn(...args: ClassNameConfig[]): string {
  return args
    .filter(isApplicable)
    .flatMap((arg) => (typeof arg === "object" ? objectToClasses(arg) : [arg]))
    .join(" ");
}

function isApplicable(arg: ClassNameConfig): arg is string | object {
  return !!arg && arg !== true;
}

function objectToClasses(obj: object): string[] {
  return Object.entries(obj)
    .filter(([, enabled]) => enabled)
    .map(([className]) => className);
}

First, we discard any arguments which are not applicable using a filter clause. We only keep truthy values, except for strictly true values (these need to be filtered out for the logical OR pattern to work). We use a type guard isApplicable for this to keep TypeScript happy since it cannot infer the type narrowing of the filter clause.

Now we are only left with string and object arguments. We simply pass the string arguments through while we transform the object arguments to an array of class strings with the objectToClasses function. This is quite straightforward as we simply need to filter the object's entries for truthy property values (named enabled in the filter clause) and then unwrap the respective property names which are the desired class strings to be applied.

Finally, the flatMap flattens the arrays returned by the callback function and the join transforms the resulting array into the final, space-separated class string.

Using a package vs. your own function

The functionality shown here is for the most part offered by packages like classnames or clsx. I used to use classnames intensively. Recently I discovered clsx, which is a faster alternative to classnames as shown in their benchmarks.

However, as mentioned in the beginning, I decided now to roll my own implementation for this utility function in my personal projects. Having a dependency generally is a maintenance burden and risk, and might not be worth it for trivial functionality like this. Plus these packages do not offer the logical OR pattern, which I like to use.

On the other hand, the performance of my own implementation might not be optimal (since it uses flatMap which creates many intermediate arrays). If needed this should be benchmarked and could then be improved based on this, probably at the cost of code readability. The clsx source code provides good inspiration for a performant implementation.

How do you concatenate classes in your projects? Let me know on Twitter!


felixmokross.dev