BlogReactJS

Using arrays for multiple conditional class names in React

Written by Codemzy on March 21st, 2024

The `className` prop can get pretty messy in React once you start adding multiple conditional class names to it. Here's how I use an array to avoid extra whitespace and confusing code.

Here's a little niggle I had for a while in React. Multiple conditional class names! When you create components that you use in lots of different places, but they have quite a few different styling options, it can get kinda complicated. Especially if you use a utility CSS framework like TailwindCSS.

You either end up with lots of extra whitespace when classes are not needed or lots of .trim() workarounds to remove the extra whitespace!

In this blog post I'll show you how I went from this:

className={`m-1 inline-block px-3 py-2 font-medium ${colors[color] ? colors[color] : colors["gray"]} ${rounded ? "rounded-full" : ""} ${size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base"} ${capitalize ? ["capitalize"] : ""} ${uppercase ? ["uppercase"] : ""} ${lowercase ? ["lowercase"] : ""}`}

To this:

className={classNames.join(" ")}

With the help of an array!

As a bonus, it also means I can pass a className prop from the parent component to add an extra class or two without breaking my styles!

Let's see how it works:

The problem with string concatenation

Why not use string concatenation? After all, className needs to be a string, and we are just adding a bunch of class names together to make that string!

Using string concatenation works, but you'll need to manage some extra spaces. And hey, if extra spaces in your className attribute don't bother you, then this could be the way you go. Especially with template literals.

Let me show you some examples of the type of things that used to bother me.

If it's just one conditional class name, I used to do something like:

<div className={"px-3 py-2 font-medium" + (tiny ? " text-xs" : "") }>

Make sure you add that space before the new class (" text-xs") so that your CSS classes don't merge as "font-mediumtext-xs", because that will break both classes!

That works pretty well, and even with multiple classes and ternary class names, you can string things together, but it can end up looking pretty messy.

Then I switched to template literals (strings using backticks), which works great for ternary class names like:

<div className={`px-3 py-2 font-medium ${tiny ? "text-xs" : "text-sm"}`}>

But not so much for one conditional class name. If tiny is undefined, you end up with an annoying space at the end of your className. And that bothers me more than it probably should!

<div className={`px-3 py-2 font-medium ${tiny ? "text-xs" : ""}`}>
// "px-3 py-2 font-medium "

So I add a trim() at the end to remove that extra whitespace.

<div className={`px-3 py-2 font-medium ${tiny ? "text-xs" : ""}`.trim()}>
// "px-3 py-2 font-medium"

And that works, but what about multiple conditional class names? Because trim() only removes whitespace from the ends of the string. You might have multiple conditional class names throughout your className attribute.

Using arrays for conditional class names

Ok, so far we've looked at a couple of conditional class names, and you might be thinking "That's not too bad". But let's look at a real-world example, so you can see how using an array can help with multiple conditional class names (and avoid a bunch of extra spaces in our className string!)

Here's a Badge component.

const colors = {
    gray: 'bg-gray-50 text-gray-700',
    green: 'bg-green-50 text-green-500',
    red: 'bg-red-50 text-red-500',
    yellow: 'bg-yellow-50 text-yellow-500',
    indigo: "bg-indigo-50 text-indigo-500",
    orange: "bg-orange-50 text-orange-500",
    pink: "bg-pink-50 text-pink-500",
    purple: "bg-purple-50 text-purple-500",
    blue: "bg-blue-50 text-blue-500",
    teal: "bg-teal-50 text-teal-500",
};

function Badge({
    color,
    size,
    rounded = true,
    capitalize,
    uppercase,
    lowercase,
    children, 
    ...props 
}) {
    
    // TO DO
    return (
        <div className="m-1 inline-block px-3 py-2 font-medium" {...props}>
            { children }
        </div>
    );
};

Our badges have a few options. They can be different colours, different sizes, capitalised, or lowercase.

When I try and put this all together with string concatenation, it looks like this:

className={`m-1 inline-block px-3 py-2 font-medium ${colors[color] ? colors[color] : colors["gray"]} ${rounded ? "rounded-full" : ""} ${size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base"} ${capitalize ? ["capitalize"] : ""} ${uppercase ? ["uppercase"] : ""} ${lowercase ? ["lowercase"] : ""}`}

And in our Badge component:

const colors = {
    gray: 'bg-gray-50 text-gray-700',
    green: 'bg-green-50 text-green-500',
    red: 'bg-red-50 text-red-500',
    yellow: 'bg-yellow-50 text-yellow-500',
    indigo: "bg-indigo-50 text-indigo-500",
    orange: "bg-orange-50 text-orange-500",
    pink: "bg-pink-50 text-pink-500",
    purple: "bg-purple-50 text-purple-500",
    blue: "bg-blue-50 text-blue-500",
    teal: "bg-teal-50 text-teal-500",
};

function Badge({
    color,
    size,
    rounded = true,
    capitalize,
    uppercase,
    lowercase,
    children, 
    ...props 
}) {
    
    return (
        <div className={`m-1 inline-block px-3 py-2 font-medium ${colors[color] ? colors[color] : colors["gray"]} ${rounded ? "rounded-full" : ""} ${size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base"} ${capitalize ? ["capitalize"] : ""} ${uppercase ? ["uppercase"] : ""} ${lowercase ? ["lowercase"] : ""}`} {...props}>
            { children }
        </div>
    );
};

Some options, like colours, will always fall back to default (grey) and will always exist in the className string, but some are optional, and might not exist at all.

When we come to use the <Badge> component, if we don't pass any props, and if those options extra don't exist, we end up with quite a few extra spaces in the class string:

"m-1 inline-block px-3 py-2 font-medium bg-gray-50 text-gray-700  text-base   "

And I also don't feel like the original code is all that readable.

So what if we use an array instead?

let options = [ 
  colors[color] ? colors[color] : colors["gray"],
  ...(rounded ? "rounded-full" : []),
  size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base",
  ...(capitalize ? ["capitalize"] : []),
  ...(uppercase ? ["uppercase"] : []),
  ...(lowercase ? ["lowercase"] : []),
];

I've conditionally added the optional props to the array using a ternary operator. If they don't exist as a prop, they don't get added to the array.

So only the classes you need are in the array.

Now you have an array of classes, and can join them together with a space (.join(" ")) to make a string!

className={`m-1 inline-block px-3 py-2 font-medium ${options.join(" ")}`}
// "m-1 inline-block px-3 py-2 font-medium bg-gray-50 text-gray-700 text-base"

No more extra whitespace in the output, and much more readable!

const colors = {
    gray: 'bg-gray-50 text-gray-700',
    green: 'bg-green-50 text-green-500',
    red: 'bg-red-50 text-red-500',
    yellow: 'bg-yellow-50 text-yellow-500',
    indigo: "bg-indigo-50 text-indigo-500",
    orange: "bg-orange-50 text-orange-500",
    pink: "bg-pink-50 text-pink-500",
    purple: "bg-purple-50 text-purple-500",
    blue: "bg-blue-50 text-blue-500",
    teal: "bg-teal-50 text-teal-500",
};

function Badge({
    color,
    size,
    rounded = true,
    capitalize,
    uppercase,
    lowercase,
    children, 
    ...props 
}) {
    let options = [ 
      colors[color] ? colors[color] : colors["gray"],
      ...(rounded ? "rounded-full" : []),
      size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base",
      ...(capitalize ? ["capitalize"] : []),
      ...(uppercase ? ["uppercase"] : []),
      ...(lowercase ? ["lowercase"] : []),
    ]
    
    return (
        <div className={`m-1 inline-block px-3 py-2 font-medium ${options.join(" ")}`} {...props}>
            { children }
        </div>
    );
};

Passing the className prop without overriding the defaults

Do you know what else this is good for? Have you ever been in a situation where you've wanted to pass a few extra classes for a unique use-case, but then realised it's going to override all your defaults?

With TailwindCSS, you can just add an extra class to add a new style. For example, if I want to give a badge a shadow, no problem, I can add the "shadow" class.

<Badge className="shadow">My Badge</Badge>

Except, there is a problem. Now we are passing the className prop from the parent component, it will override the className prop in the Badge component. All those default classes and options we've just coded are gone!

We are left with just a shadow, and nothing else!

But don't despair! With the array we have, we can take the className prop and add that to our options array. In fact, I'm going to rename that array classNames and put all of our class names in it!

function Badge({
    color,
    size,
    rounded = true,
    className,
    children, 
    ...props 
}) {
    let classNames = [ 
      "m-1 inline-block px-3 py-2 font-medium", // base styles
      colors[color] ? colors[color] : colors["gray"],
      ...(rounded ? "rounded-full" : []),
      size === "xs" ? "text-xs" : size === "sm" ? "text-sm" : "text-base",
      ...(className ? className : []), // for extra custom class names
    ]
    
    return (
        <div className={classNames.join(" ")} {...props}>
            { children }
        </div>
    );
};

And I've been able to remove the capitalize etc since they can just be passed as classes now instead of props.

<Badge className="shadow capitalize">My Badge</Badge>

Now we get all the usual Badge class names, plus our "shadow" and "capitalize" class names added to the end (instead of overriding as they did before).

Joining arrays for the win!