A simple strategy for structuring TailwindCSS classnames
Nikolaus Rademacher
Posted on April 13, 2021
This is the third article of my small series about TailwindCSS. If you have not done so already, check out my other posts.
Anyone who has proposed to use TailwindCSS for their project has probably heard something like this:
"Ugh, this looks like inline styles!"
"How should I keep an overview with so many classnames in my component?"
"This seems hard to maintain…"
Yes, I understand these concerns. With Tailwind's utility-first approach, the default procedure is to write any utility-classname directly into the component's markup. With more complicated components this can quickly come out of hand.
In today's post, we will look at a possibly better solution which I am using for my projects for a while now.
A simple example
Let's take this Navigation
component as an example:
const Navigation = ({ links }) => {
const router = useRouter()
return (
<nav className="container">
<ul className="flex flex-col justify-end list-none sm:flex-row">
{links.map((link, index) => {
return (
<li
key={index}
className="mb-3 sm:ml-3 sm:mb-0 even:bg-gray-50 odd:bg-white"
>
<a
className={`text-black font-bold inline-block rounded-full bg-yellow-400 py-1 px-3 ${
router.pathname === link.path
? 'text-white'
: 'hover:bg-yellow-500'
}`}
href={link.path}
>
{link.name}
</a>
</li>
)
})}
</ul>
</nav>
)
}
What can we do to not let the component look so messy?
My first rule of thumb is: Do any calculations before your render / return function and only use these calculated flags in your render. That applies for the router.pathname === link.path
condition – let's move it into a const
and name it isActive
.
And while we are at it, let's move the className
definitions to const
s as well – just name them after their according HTML element (another reason for using semantic elements instead of a bunch of div
s ;)):
const Navigation = ({ links }) => {
const router = useRouter()
const navClassNames = 'container'
const listClassNames = 'flex flex-col justify-end list-none sm:flex-row'
return (
<nav className={navClassNames}>
<ul className={listClassNames}>
{links.map((link, index) => {
const isActive = router.pathname === link.path
const listItemClassNames =
'mb-3 sm:ml-3 sm:mb-0 even:bg-gray-50 odd:bg-white'
const anchorClassNames = `text-black font-bold inline-block rounded-full bg-yellow-400 py-1 px-3 ${
isActive ? 'text-white' : 'hover:bg-yellow-500'
}`
return (
<li key={index} className={listItemClassNames}>
<a className={anchorClassNames} href={link.path}>
{link.name}
</a>
</li>
)
})}
</ul>
</nav>
)
}
That looks better already, but there’s still room for improvement.
Use .join(" ")
Instead of writing long strings of classNames, let's write arrays and concatenate them automatically. The good thing about arrays is that you can also add entries conditionally – and therefore get rid of the template literal condition:
const Navigation = ({ links }) => {
const router = useRouter()
const navClassNames = 'container'
const listClassNames = [
'flex',
'flex-col',
'justify-end',
'list-none',
'sm:flex-row',
].join(' ')
return (
<nav className={navClassNames}>
<ul className={listClassNames}>
{links.map((link, index) => {
const isActive = router.pathname === link.path
const listItemClassNames = [
'mb-3',
'sm:ml-3',
'sm:mb-0',
'even:bg-gray-50',
'odd:bg-white',
].join(' ')
const anchorClassNames = [
'text-black',
'font-bold',
'inline-block',
'rounded-full',
'bg-yellow-400',
'py-1',
'px-3',
isActive ? 'text-white' : 'hover:bg-yellow-500',
].join(' ')
return (
<li key={index} className={listItemClassNames}>
<a className={anchorClassNames} href={link.path}>
{link.name}
</a>
</li>
)
})}
</ul>
</nav>
)
}
(One note concerning the ternary operator that conditionally adds a className: If you don't have an either/or operation, just add an empty string to the else case (e.g. isCondition ? 'myClass' : ''
) and don't rely on shorthands like isCondition && 'myClass'
. The latter would work for undefined
values but add a "false"
string to your array in case the condition is false.)
Abstract all component styles into a styles
object
Let's further work on this approach: In this example with multiple elements in one component especially it might make sense to create a styles object outside of the component's return
functions.
But there is one issue: In our anchor link styles definition we rely on having access to the isActive
flag. We can easily solve this by transforming its definitions from a string to an arrow function returning a string. With such a function you can provide any condition you need in the scope of your element's styles array:
const styles = {
nav: 'container',
ul: [
'flex',
'flex-col',
'justify-end',
'list-none',
'sm:flex-row',
].join(' '),
li: [
'mb-3',
'sm:ml-3',
'sm:mb-0',
'even:bg-gray-50',
'odd:bg-white',
].join(' '),
a: ({ isActive }) =>
[
'text-black',
'font-bold',
'inline-block',
'rounded-full',
'bg-yellow-400',
'py-1',
'px-3',
isActive ? 'text-white' : 'hover:bg-yellow-500',
].join(' '),
}
const Navigation = ({ links }) => {
const router = useRouter()
return (
<nav className={styles.nav}>
<ul className={styles.ul}>
{links.map((link, index) => {
const isActive = router.pathname === link.path
return (
<li key={index} className={styles.li}>
<a className={styles.a({ isActive })} href={link.path}>
{link.name}
</a>
</li>
)
})}
</ul>
</nav>
)
}
Another note here: I've put the flag into an object instead of directly into the arguments list (({ isActive })
instead of (isActive)
). This makes sense because it is easier to maintain: Otherwise you would have to think of the particular order of your flags in both the function call and its definition within the styles object. With the object's destructuring syntax you can work around this issue and don't need to worry about the object entries' positions – by just adding two more characters.
Put styles into a separate file
I you want to take it even further, you could outsource your styles to a separate file with the same approach:
// Navigation.styles.js
export default {
nav: 'container',
ul: [
'flex',
'flex-col',
'justify-end',
'list-none',
'sm:flex-row',
].join(' '),
li: [
'mb-3',
'sm:ml-3',
'sm:mb-0',
'even:bg-gray-50',
'odd:bg-white',
].join(' '),
a: ({ isActive }) =>
[
'text-black',
'font-bold',
'inline-block',
'rounded-full',
'bg-yellow-400',
'py-1',
'px-3',
isActive ? 'text-white' : 'hover:bg-yellow-500',
].join(' '),
}
// Navigation.jsx
import styles from "./Navigation.styles";
const Navigation = ({ links }) => {
const router = useRouter()
return (
<nav className={styles.nav}>
<ul className={styles.ul}>
{links.map((link, index) => {
const isActive = router.pathname === link.path
return (
<li key={index} className={styles.li}>
<a className={styles.a({ isActive })} href={link.path}>
{link.name}
</a>
</li>
)
})}
</ul>
</nav>
)
}
I'm working with this approach for a while now and I really like it. It's simple and clean and it allows me to write TailwindCSS without cluttering my components with a bunch of classnames.
Other approaches
There are some other approaches that you can use instead or in combination with the above:
Use classnames()
(or clsx()
)
The classnames()
library is a simple utility to concatenate your classNames into a string. It has some additional functions built in that might come in handy.
clsx()
has the same API but comes with a smaller bundle size:
These libraries makes sense especially when dealing with many conditions like the isActive
one in the example above or with nested arrays that you would need to flatten otherwise.
For most cases I'd say that joining an array like above will do the work and that you don't need any additional package for that – but for bigger projects it might make sense to embrace the API of those libraries.
brise
Another interesting approach is pago's brise:
It is using template literals to work with Tailwind styles. And it even allows you to add custom CSS by using emotion's css
utility.
It's also definitely worth checking out.
I hope this post inspired you writing cleaner components when using TailwindCSS. If you have any other recommendations feel free to add them to the comments!
Posted on April 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.