How to handle classes in large components with Tailwind and React ?

8 min read / January 16, 2023

As a front-end developer, one of my goals on every project is to have clean, concise and easily maintainable code. And to be honest, with Tailwind it's sometimes very difficult to achieve this goal, especially when creating large components with multiple variations and states.

But don't get me wrong, I love working with Tailwind ❤️ and have been using it on all my projects since its release, I actually built this blog with it!

Let's get back on topic though and take the example of a simple button, this component is going to be visually different depending on its variant, it can also be disabled and have different sizes. All these props will generate a lot of classes, resulting very quickly in a big mess in our code 🤯, especially if we want to combine props or add others later on.

What about the classnames library ?

The package classnames is excellent, and partially solves this problem by allowing us to split our Tailwind classes within our component. I used this package for a long time and was happy with the logic I had in place to organize my classes.

Let's look at the simplified example of my design system button created with classnames, try changing the variant, size and disabled props :

import { FunctionComponent } from 'react'
import { Button } from './Button'
              
const App: FunctionComponent = () => {
  return (
    <div className="h-screen dark:bg-gray-800 grid gap-6 content-center justify-center">
      <Button size="sm">Button</Button>
      <Button>Button</Button>
      <Button disabled>Button</Button>
      <Button size="lg">Button</Button>
      <Button size="sm" variant="secondary">Button</Button>
      <Button variant="secondary">Button</Button>
      <Button variant="secondary" disabled>Button</Button>
      <Button size="lg" variant="secondary">Button</Button>
    </div>
  )
}
    
export default App;

As you can see, the above code works perfectly, and to be honest, it's already clean, well organized and easily maintainable.

However, several details can be improved:

  1. The conditional syntax in the classnames function is quite cumbersome, especially if we add more props.
  2. We have to manually assign classes to the props, and manually type the visual aspects of our component
  3. The typing of our classes object must be added but will be tedious to set up and will duplicate our interface defined above

Can't we just get Stiches but for Tailwind 🙏 ?

Some of you know or use Stiches, a CSS-in-TS library that offers a very nice DX, and allows us to better organize the variants of our components as well as to combine and type them automatically: 🤩.

Unfortunately with Tailwind we can't use Stiches... Or maybe we can 🤔 ?

Getting started with cva

Let's convert our code with cva! First let's install the yarn add class-variance-authority library. cva, like Stiches, is based on the creation of variants and the composition of them. The structure I had in place before was already partly based on this principle, so it's quite simple to convert it with cva.

import { FunctionComponent } from 'react'
import { Button } from './Button'
              
const App: FunctionComponent = () => {
  return (
    <div className="h-screen dark:bg-gray-800 grid gap-6 content-center justify-center">
      <Button size="sm">Button</Button>
      <Button>Button</Button>
      <Button disabled>Button</Button>
      <Button size="lg">Button</Button>
      <Button size="sm" variant="secondary">Button</Button>
      <Button variant="secondary">Button</Button>
      <Button variant="secondary" disabled>Button</Button>
      <Button size="lg" variant="secondary">Button</Button>
    </div>
  )
}
    
export default App;

We have the same result as before, but with one detail: our code is much more organized and clean! Let's explain in detail what we have done.

In our Button.styles.ts file we have replaced the classes object with a container that uses the cva function. This function accepts two arguments: the first one is our base classes, i.e. the ones that will be present on our button component regardless of the variant. The second argument is an object made of three objects that I will explain in more detail.

Variants

In the variants object I define all the variants of my component: variant, size and disabled.

As you can see, for our variant props I define very few classes because I don't want to have class conflicts when my button is disabled or not. The rest of the classes will be added in our next coumpoundVariant object.

Compound variants

It's within the compoundVariants object that I place the majority of my classes. Here for example I only want to apply my classes according to my variant and disabled props, and as said earlier, I don't want them to conflict, so I define them separately.

Default variants

Finally the defaultVariants object allows me, as its name indicates, to specify which variants will be applied by default :

Automatic typing

As you may have noticed, our types have been switched from manual to automatic input using cva and its VariantProps type. We just need to extend the type with the constant we created just now VariantProps<typeof buttonStyles>. So my interface looks like this now:

Automatic class association

The biggest improvement we made is the automatic typing. We went from a manual props - classes association to an automatic association thanks to cva and the constant we created above, which gives us :

Conclusion

Using cva actually allows us to have a single source of truth for our styles, props, and types, making our code much more readable, maintainable, and offering a DX similar to what we find with Stiches.

Here we covered the basics of cva and used a fairly simple example, to get a feel for the concepts of the library, but trust me, it's when you use it on large components that you'll fall in love with the package and won't be able to live without it!

Hats off to Joe Bell and all the contributors who maintain and add more and more features to cva 🎉 !

GameBoy

© 2023 - Simon Bellucci