DEV Community

Cover image for Composable Tailwind
Nico Bachner
Nico Bachner

Posted on • Updated on • Originally published at nicobachner.com

Composable Tailwind

Tailwind CSS is one of the most popular Atomic CSS frameworks. The API is excellent, adding minimal mental overhead and no code bloat once set up. Their documentation is extensive, covering their API, as well as nearly all other use cases imaginable.However, there is a relatively common issue that is not mentioned – one that is inherent to all class-based styling solutions in JSX1. This issue is composing styles.

Utility classes work great when there is a single source of truth for styling. However, once faced with more complex scenarios such as conditional styling or prop-based styling, one can easily end up with a mess of template literals:

<div
  className={`m-4 p-4 ${condition1 == true ? 'text-white' : 'text-black'} ${
    condition2 == true ? 'bg-white' : 'bg-black'
  }`}
/>
Enter fullscreen mode Exit fullscreen mode

The above can quickly become unreadable.

Fortunately, there are other ways we can approach this. As I have previously proposed in my guide to CSS Modules, we can instead reach for string concatenation to compose classes. The above example would now look like this:

<div
  className={[
    'm-4 p-4',
    condition1 == true ? 'text-white' : 'text-black',
    condition2 == true ? 'bg-white' : 'bg-black',
  ].join(' ')}
/>
Enter fullscreen mode Exit fullscreen mode

This approach brings a few clear benefits to the table:

  • More readable code
  • Grouping by source
  • Clear order of execution2

Let us apply this to a real-world example. We'll have a button with two variants (primary and secondary), with an escape hatch for the possibility of customising the button, should that be necessary (this happens more often than you would expect).

// Button.tsx
type ButtonProps = {
  variant: 'primary' | 'secondary'
  className?: string
}

export const Button: React.FC<ButtonProps> = ({
  children,
  property1,
  property2,
  className,
}) => (
  <button
    className={[
      'rounded border border-black px-8 py-4',
      variant == 'primary' ? 'bg-black' : 'bg-inherit',
      className,
    ].join(' ')}
  >
    {children}
  </button>
)
Enter fullscreen mode Exit fullscreen mode

Now we can consume this button as expected:

<Button variant="primary" className="mt-4">
  {children}
</Button>
Enter fullscreen mode Exit fullscreen mode

  1. This is not an issue with CSS-in-JS styling solutions, such as Stitches. While they compile down to classes at the end of the day, the authoring is done as individual styles (in JS Object format), which are much easier to compose. 

  2. What is meant by this is that the styles at the end override the styles at the beginning. 

Top comments (0)