DEV Community

loading...

A simple strategy for structuring TailwindCSS classnames

wheelmaker24 profile image Nikolaus Rademacher ・5 min read

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!

Discussion (15)

pic
Editor guide
Collapse
maxart2501 profile image
Massimo Artizzu

How come every time I read an article about Tailwind it's about trying to solve its problems? And most of the times the solutions are pretty much what plain CSS can do just fine?

I was wondering... Why is this good:

const listItemClassNames =
  'mb-3 sm:ml-3 sm:mb-0 even:bg-gray-50 odd:bg-white'
Enter fullscreen mode Exit fullscreen mode

while this is not?

.list-item {
  margin-bottom: $size-3;
  &:nth-child(odd) {
    background: $gray-50;
  }
  &:nth-child(even) {
    background: white;
  }
  @media (max-width: $small-size) {
    margin-left: $size-3;
    margin-bottom: 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

Just because it's more compact?

Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author

The idea is to have a constrained system that only allows predefined values (the ones you have defined in the tailwind.config.js file). You are forced to design your components based on these "tokens". In a way you could achieve the same with SCSS / Sass mixins.

Another reason is that the compiled CSS file will stay small in size no matter how many components you have. With standard CSS approaches you would need to have specific style definitions for each component that will let the CSS output file grow in size.

Also when sticking to Tailwind you won't get in trouble with specificity issues (CSS overwrites and alike).

It's definitely a paradigm shift and can feel strange in the beginning. For me the constraints argument is the strongest, especially when using it in design systems.

Check out the official explanation of Tailwind's utility-first concept here:
tailwindcss.com/docs/utility-first

If you like, check out my other posts on Tailwind:

Collapse
andreidascalu profile image
Andrei Dascalu

It is, mostly because your organizational solutions go well with React/Angular. Do that in any other kind of web app, templating system and so on and it quickly gets out of hand. You need to manage your lists of classes in code and break the whole point of having a presentation layer.
Scss still works better. If there's a cross-apptype solution, I'd rather stick with that since it's usable in more cases.

Thread Thread
wheelmaker24 profile image
Nikolaus Rademacher Author

Fair point – these tipps will only help if you are using a framework / library that uses JavaScript / JSX for the "presentation layer".

Collapse
maxart2501 profile image
Massimo Artizzu

I've already read the official documentation of Tailwind. And several posts already, including yours. Every single point that had been made has been debunked.

The fact that the final CSS file is smaller has meaningless advantages. Styles are still a very small payload and computational overhead for the web. A single picture could weigh more than all the stylesheets in an application, and if you're using React or similar the impact of JavaScript on the CPU is orders of magnitude bigger.

Moreover, using SCSS mixins may lead to code repetition, but compression algorithms are very good at deflating repeated text.

Specificity is a powerful feature of CSS. I've never understood why one should give up understanding and using it. If one has troubles with it, they could either learn how to deal with it or... learn Tailwind?

And again, if you still have specificity problems in an era of encapsulated styles, is pretty clear you're using the wrong approach.

I think I'll stick to good old SCSS for the foreseeable future. And advise against Tailwind and similar utility-first patterns, that in my view solve nothing but the need to develop a good methodology.

That you still need with Tailwind anyway, just like your posts have proven once again.

Thread Thread
wheelmaker24 profile image
Nikolaus Rademacher Author

I see your points here and I was pretty much coming from the same side. I'm not afraid of CSS / specificity and alike and was always sceptical when it came to CSS-in-JS solutions as they in my view solved problems that I never had (like the scoping issue). The cascade is one of the main features in CSS and you can benefit from it a lot when using it correctly.

Also I would never say that you have to use Tailwind for every project, but when working in bigger projects with many engineers especially it is important to have constraints – and I have not found a better solution for that yet than Tailwind: It's easy to learn not only for engineers but for designers as well as it helps you to think in constrained tokens. If you have a better solution for this, I'd happy to hear it from you!

It's also fun to look behind the code of Tailwind – it has clever solutions for many problems. With that it is at least a great library to learn from and apply in your own CSS code afterwards. The way Tailwind takes advantage of CSS' core features like CSS variables / the cascade / specificity is nice to see.

Collapse
moopet profile image
Ben Sinclair

I[f] you want to take it even further, you could outsource your styles to a separate file

Have you just invented... stylesheets?

Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author

Hahaha, yupp... 😎

Collapse
zakiazfar profile image
Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author

Right, forgot about twin.macro. But with this you could also end up having cluttered components, wouldn't you?

Collapse
zakiazfar profile image
zakiAzfar

I use Framer Motion. Each of my elements look like

<motion.anyElementName />
Enter fullscreen mode Exit fullscreen mode

This is messy, with styled-components it become easier I can use

<appropriateComponentName />
Enter fullscreen mode Exit fullscreen mode
Collapse
aalphaindia profile image
Pawan Pawar

keep sharing!!

Collapse
shareef profile image
Mohammed Nadeem Shareef

I like to use componentName.module.css

  • We will have separate files
  • We can combine the CSS power with the tailwind
  • We will have a simple but powerful structure also.
Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author • Edited

And within the CSS file you would @apply the Tailwind utility-classes? That's definitely a good approach. Although you will still end up with specific stylesheets for each component. Would be similar to SCSS with mixins IMO.

Collapse
shareef profile image
Mohammed Nadeem Shareef

yeah! it will be similar to SCSS and we can use the power of scss and tailwindcss combined, and yes I use @apply.