Published on

Using Tagged Template Literals for clsx and Tailwind

5 min read

Authors

TL;DR

To use clsx and Tailwind like this:

import cx from 'cx'

const containerClasses = cx`
    container
    mx-auto
    flex
    justify-between
    items-center
    ${isMobileMenuOpen ? 'px-2' : 'px-4'}
  `

Create a helper function (ex: /lib/cx.{ts,js})

  • Typescript
import clsx, { ClassValue } from 'clsx'

type TemplateLike = TemplateStringsArray | ClassValue
type ParamsLike = ClassValue[]

const cx = (template: TemplateLike, ...params: ParamsLike): string => {
  if (typeof template === 'string') return clsx(template, ...params)

  template = Array.isArray(template) ? template : [template]
  let merged = template.join('') + params.join('')
  merged = merged.replace(/\s+/g, ' ').trim()

  return clsx(merged)
}

export default cx
  • Javascript
import clsx from 'clsx'

const cx = (template, ...params) => {
  let merged = template.join('') + params.join('')
  merged = merged.replace(/\s+/g, ' ').trim()

  return clsx(merged)
}

export default cx
  • Playground

Tailwind

Tailwind provides us frontend devs to co-locate our styling and our component in the same place, it makes related code closer together.

import { useState } from 'react'

function Navbar() {
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)

  const toggleMobileMenu = () => {
    setIsMobileMenuOpen(!isMobileMenuOpen)
  }

  // Define an array of navigation links
  const navigationLinks = [
    { text: 'Home', url: '#' },
    { text: 'About', url: '#' },
    { text: 'Services', url: '#' },
    { text: 'Contact', url: '#' },
  ]

  return (
    <nav className="bg-blue-500 p-4">
      <div className="container mx-auto flex justify-between items-center">
        <div className="text-white text-lg font-semibold">My Website</div>
        <div className="hidden md:flex space-x-4">
          {navigationLinks.map((link, index) => (
            <a
              key={index}
              href={link.url}
              className="text-white hover:text-gray-300 uppercase tracking-wide font-medium"
            >
              {link.text}
            </a>
          ))}
        </div>
        <div className="md:hidden">
          <button
            className="text-white focus:outline-none focus:shadow-outline"
            onClick={toggleMobileMenu}
          >
            <i className={`fas ${isMobileMenuOpen ? 'fa-times' : 'fa-bars'}`}></i>
          </button>
        </div>
      </div>
      {isMobileMenuOpen && (
        <div className="md:hidden">
          <div className="container mx-auto mt-4 space-y-2">
            {navigationLinks.map((link, index) => (
              <a
                key={index}
                href={link.url}
                className="block text-white hover:text-gray-300 uppercase tracking-wide font-medium"
              >
                {link.text}
              </a>
            ))}
          </div>
        </div>
      )}
    </nav>
  )
}

export default Navbar

As you can see from the code above, the styling, component layout, and UI logic exists in the same file. But you can also notice that Tailwind classes can be long, especially when the UI is relatively complex.

And that is the inherent problem of Tailwind:

Long Classes

clsx

There are several approaches to reduce this problem, one of them is [clsx]. clsx is a small utility package (234B) to help compose css classes in general, it primarily used for applying conditional classes.

From the Navbar component above, we can refactor it to use clsx:

...

 const navigationLinks = [
	...
 ];

  const containerClasses = clsx(
    'container',
    'mx-auto',
    'flex',
    'justify-between',
    'items-center',
    {
      'px-4': !isMobileMenuOpen,
      'px-2': isMobileMenuOpen,
    }
  );

  const buttonClasses = clsx(
    'text-white',
    'focus:outline-none',
    'focus:shadow-outline',
    {
      'block': isMobileMenuOpen,
      'hidden': !isMobileMenuOpen,
    }
  );

...
return (
    <nav className="bg-blue-500">
      <div className={containerClasses}>
        ...
        <div className="md:hidden">
          <button
            className={buttonClasses}
            onClick={toggleMobileMenu}
          >
            <i className={`fas ${isMobileMenuOpen ? 'fa-times' : 'fa-bars'}`}></i>
          </button>
        </div>
      </div>
     ...
    </nav>
  );

The component is now a bit easier to read after we move the classes to the top.

Moving the classes to a different place is just like having dirt all over your room and just sweeping it to the corner, it's still dirty!

Yes, using clsx doesn't solve the fact that Tailwind still make our code "dirty". But "dirty" itself is subjective. I think the benefits of Tailwind still outweigh it's limitation. In fact, this arguments is one of the most common reason why devs dislikes Tailwind. But it is a discussion for another time. Back to the main topic.

That backtick syntax from styled-components (Tagged Template Literal)

Personally, being a lazy efficient devs, writing the class names as arrays is a bit cumbersome. Then I remember how styled-components uses this weird backtick syntax:

const Button = styled.button`
  background: transparent;
  border-radius: 3px;
  border: 2px solid #bf4f74;
  color: #bf4f74;
  margin: 0 1em;
  padding: 0.25em 1em;
`

After reading some more, especially this great article from Max Stoibers and the MDN Reference. I found that the syntax is "Tagged Template Literal" and "Tag Function". I recommend you to have a read at that article.

Turns out that this syntax was introduced in ES6. What happens is (Borrowing from Max's blog post):

Basically, strings inside the tag will be received as array, separated by any template string which will be passed as separate arguments following it.

My Approach

With all that being said, the goal that I want to achieve is writing clsx but with tagged template literal. It doesn't work out of the box. So I created a utility function that make tagged template literal works with clsx.

In short, what I want to do instead of this:

const containerClasses = clsx('container', 'mx-auto', 'flex', 'justify-between', 'items-center', {
  'px-4': !isMobileMenuOpen,
  'px-2': isMobileMenuOpen,
})

is this:

import cx from '@/lib/cx'

const containerClasses = cx`
    container
    mx-auto
    flex
    justify-between
    items-center
    ${isMobileMenuOpen ? 'px-2' : 'px-4'}
  `

I usually create a lib/cx.{ts,js} file containing:

  • Typescript
import clsx, { ClassValue } from 'clsx'

type TemplateLike = TemplateStringsArray | ClassValue
type ParamsLike = ClassValue[]

const cx = (template: TemplateLike, ...params: ParamsLike): string => {
  if (typeof template === 'string') return clsx(template, ...params)

  template = Array.isArray(template) ? template : [template]
  let merged = template.join('') + params.join('')
  merged = merged.replace(/\s+/g, ' ').trim()

  return clsx(merged)
}

export default cx
  • Javascript
import clsx from 'clsx'

const cx = (template, ...params) => {
  let merged = template.join('') + params.join('')
  merged = merged.replace(/\s+/g, ' ').trim()

  return clsx(merged)
}

export default cx

Pretty straightforward, I join the template and the following params (string) to a single string.

I also added the replace and trim function since without it, the template will include newlines and multiple white spaces to the final CSS class:

<div
  class="
    container
    mx-auto
    flex
    justify-between
    items-center
  "
>
  ...
</div>

After the the replace and trim

<div class="container mx-auto flex justify-between items-center">...</div>

Thanks for reading!