A simple strategy for structuring TailwindCSS classnames

wheelmaker24

Nikolaus Rademacher

Posted on April 13, 2021

A simple strategy for structuring TailwindCSS classnames

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 consts as well – just name them after their according HTML element (another reason for using semantic elements instead of a bunch of divs ;)):

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

(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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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(' '),
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
wheelmaker24
Nikolaus Rademacher

Posted on April 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related