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!