- Published on
Using Tagged Template Literals for clsx and Tailwind
5 min read
- Authors
- Name
- Syakhisk Al Azmi
- @syakhiskk
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.
styled-components
(Tagged Template Literal)
That backtick syntax from 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!