How to keep Tailwind DRY

There’s many complaints I see online about Tailwind: it’s WET, not DRY, it’s the same as inline styles, you can’t make global changes, and it’s hard to read. I understand how this may seem to be the case when you first start working with Tailwind. The thing to bear in mind is Tailwind is nothing like traditional CSS, and you shouldn’t treat it as such.

There’s lots of ways that Tailwind can benefit all of us, such as its tiny bundle size and super-fast prototyping capabilities. But we can only get these benefits if we use it in the right situation; used in the wrong context, Tailwind will only bring you headaches.

When is the wrong time to use Tailwind CSS?

The first thing I would recommend against using Tailwind for is plain HTML static websites. When you’re building a static site, you will inevitably end up copy and pasting HTML, since there’s likely to be more than one occurrence of the same component/section on a single page.

This is totally fine if you’re using a traditional CSS methodology such as BEM: your CSS and HTML exist completely separately, so you can rely on your CSS as your single source of truth for how your website will look. If you change a CSS class, the change will be reflected everywhere the class is used without having to update the HTML. In this way, it doesn’t really matter too much if you’ve copy and pasted some HTML.

// you can copy and paste these classes anywhere
<button class="button button--negative"></button>
<button class="button button--negative"></button>
// but you could also break rules like this
<div class="button"></div>

This is about as much as you can do when it comes to separation of concerns with CSS and plain HTML. Personally, I still think this method isn’t properly DRY since you’re copy and pasting the same code in multiple places, but it’s about the best you can do with basic HTML- something that never sat right with me while I was learning CSS. To make this system truly DRY, you would want to use some form of templating or a component-based framework so you could only write HTML for a section one time, and then re-use the component wherever you like. This brings me onto…

When is the right time to use Tailwind CSS?

I’m glad you asked! If you don’t want to repeat yourself when building websites with Tailwind, you’re probably going to want to use some kind of JavaScript framework. Whether it’s React, Vue or some other new-fangled framework, the important thing is that you can build JS components which can be reused over and over. You might be able to get it to work with PHP templates, but I’d say this method is best for JavaScript since you can keep your HTML, JS and CSS all in the same file.

That’s the real way Tailwind should be used: as an entirely different paradigm where separation of concerns doesn’t mean separating HTML, CSS and JS, it means separating entire components instead, and keeping everything related to that component inside one file or folder. It’s a pretty different way of working to how we’re used to with its own challenges, but this method has some great benefits:

  • Components can behave independently of each other and be used across different projects without much hassle
  • Components can be tested on their own so you don’t need to worry about things changing later on
  • Prototyping is much faster since you don’t need to write a custom class for every single element
  • Full access to use JavaScript for more advanced conditional styling than with regular HTML
  • Encourages component composition — once you have a bunch of components, it’s easy to build pages or even new variants of components by combining what you already have

Embracing component-based architecture

Once you have your HTML, JavaScript and CSS all in one place, you’ll realise it’s much easier to contain components within their own folders rather than having resources stretched across your whole project in different file trees. Working in this way opens up new opportunities, such as being able to use JavaScript to dictate your styles and building more complex logic for views.

Here’s some tips to help you adjust to component-based development:

1. Break components down into small, reusable pieces

Have you ever noticed, when looking at a design, that there tend to be lots of repeating patterns? You can take advantage of this with class composition. It’s common to see a 50/50 layout with text on one side, and some type of media on the other. I tend to call them SplitContent blocks. There are often variants on this, perhaps some of the text is a different size or the media slot is filled with a carousel instead of an image.

Instead of building two components that use the exact same styles for the most part, you could create a container component with props, slots where you can add any kind of content. You could set up logic for your styles inside — maybe you want a prop that changes which side the content will appear on, or add padding to a certain side. Alternatively, you could just add a prop which can be passed a string of class names, giving you the ability to customise the container as it’s used in different contexts.

A page broken into smaller pieces

For areas where I want to use SplitContent as a dynamic content block with a CMS such as WordPress, I might create a Handler component which breaks down the style options defined in the CMS and passes on the relevant combinations of components.

You might, for example, want your client to only have access to one SplitContent component in the CMS, but have the choice to create many different layouts using that one component. Some of the choices might include:

  • Which type of content do you want on each side?
  • Which side should each content type be on?
  • Does this component need a different colour scheme?

These options can be taken in by the component Handler and it will return the correct layout, while keeping all this logic contained within itself so the other components can still be used across different components.

I usually keep everything related to SplitContent under one folder, and add a subfolder of smaller pieces which make up the main components:

Folder structure showing 'Component/sub' folders for smaller pieces of a component

This is just one example; essentially, your components should all have a single purpose so it’s easier to build larger and more complex components using the pieces you have created.

2. Use JS to build class lists

If you find Tailwind hard to read, you’re not alone. It’s one of the most common complaints and I can understand why: you have to read each class to understand what’s going on, which doesn’t work for everyone.

It may help to rely on JavaScript to build your class names. I often prefer this method over composing new CSS classes for the sake of it, especially when they might only be used in one place. Some people might say this is the same as using the @apply directive, but if the class isn’t going to be used anywhere else there’s no reason to write a whole new class for it. Writing classes with JavaScript like this helps to keep everything related to that component in a similar place, rather than placing it miles away in the CSS folder.

// components/Modal/View.jsx
export default function ModalView () {
  const modalContainerClass = "bg-white p-4 rounded shadow";
  const modalHeadingClass = "heading-1 text-darkgrey";
  return (
    <aside className={modalContainerClass}>
      <h1 className={modalHeadingClass}>...</h1>
    </aside>
  );
}

Storing classes in JavaScript variables makes it a little clearer what is trying to be accomplished with it, while also opening up the opportunity to use more advanced logic than would be possible with CSS.

3. Use props to extend components

One of the problems we encounter with Tailwind compared to normal CSS is that we lose the ability to extend a basic version of a component into a new modified version with classes:

// _button.scss
.button {
  padding: 20px;
  border: 1px solid black;
}
.button--negative {
  border-colour: red;
}
// index.html
<button class="button">Accept</button>
<button class="button button--negative">Cancel</button>

Of course we could manually add the border-red Tailwind class to any button we want to make negative, but what if there’s more than one style? What if the background and text colour also change?

// this would be a nightmare if the negative styles ever changed
<button class="p-5 border-red bg-red text-white">Cancel</button>

The solution: Extend your components using JavaScript

When we make the switch over to component-based development, we gain the ability to use JavaScript in place of CSS for creating components. Since you’re no longer tied to a separate stylesheet, you can create variants of your components by abstracting them to different files, using your base component as the starting point.

One of the most flexible ways of doing this is to pass class names down as props, and merge them with the existing classes on a component. This is an example of merging destructured props with other values, as shown on the fantastic resource reactpatterns.com.

Here’s how our button variants might look using this method:

// components/Button/index.jsx
export default function Button = ({ classnames, handleOnClick, label }) {
  const buttonClass = [
    "p-5 border-1", // default button styles
    classnames      // any additional styles
  ].join(' ');
  
  return (
    <button className={buttonClass} onClick={handleOnClick}>
      {label}
    </button>
  )
}
// components/Button/Negative.jsx
export default function ButtonNegative = (props) {
  return (
    <Button
      classnames="border-red bg-red text-white"
      {...props}
    />
  )
}

Now we can use index.jsx as the base layer of our button, and keep all the logic on that level, while also having variants of that button clearly defined without any change to functionality. This way, if the styles change later on, anywhere <ButtonNegative /> is used will reflect changes made in that file.

4. Move view logic and business logic to separate files

This is quite a general tip for working with JavaScript frameworks, but in Tailwind it can help even more so because it separates your styles from your business logic without relegating them to a completely different folder. You can enter your Button folder, and know that everything in that folder will be related to buttons.

Once you’ve got everything in one place, you can start to break it down further: In React, you can keep the way your component looks separate from how it behaves. Here’s an example of this:

// components/Carousel/View.jsx (view logic only)
export default function CarouselView ({ slides }) {
  return (
    <SomeCarouselPlugin>
      {Array.isArray(slides) && slides.map(slide => (
        <CarouselSlide {...slide} />
      ))}
    </SomeCarouselPlugin>
  )
}
// components/Carousel/Jobs.jsx (business logic only)
export default function JobsCarousel () {
  const [jobs, setJobs] = useState(null);
  
  const fetchJobs = async () => {
    const res = await request({
      url: 'my-api-url.com/jobs?limit=16',
      method: 'GET'
    })
    setJobs(res.data)
  }
  
  useEffect(() => {
    fetchJobs();
  }, [])
  
  return !!jobs ? (
    <CarouselView slides={jobs.map(job => ({
      title: job.title,
      description: job.description,
      salary: 'Up to ' + job.salary.max
    }))} />
  ) : <>Loading...</>
}

If we wanted to make another carousel which used the same styles, perhaps we want the carousel to be filled with staff members instead of jobs, we could do that by creating a new container component in Carousel/Staff.jsx.

This massively helps with breaking down huge components with hundreds or even thousands of lines, and this method means you could also include extra layers if you wanted to for even more customisation. This system of extension makes it easier to break down what a component is supposed to be doing, while making sure you don’t repeat yourself.

5. Use class composition for containers, text styles and anything used between components

That’s right: Even when components are your source of truth, there is still a place for custom classes. For example, you are likely to use a container class on many different components with a max width, margin: auto and some side padding. Since these aren’t likely to change, it makes sense to compose a new custom class using the @apply directive.

Personally, I also like to include typographic classes such as for headings, standard blocks of content and the like. These are things that don’t necessarily make a lot of sense to create a new JavaScript component for, but they combine the same styles in multiple places nevertheless.

.page-wrap {
  @apply max-w-page mx-auto px-4 tablet:px-5 laptop:px-6;
}
.paragraph {
  @apply text-16 font-body leading-loose;
}
// we can still create variants of .paragraph
<p class="paragraph text-white">Hello world!</p>

6. When composing classes, avoid using margins

You can make classes more reusable by making them position agnostic. If you leave out properties such as margins which only affect the position of the element, you are able to reuse it more often.

// _typography.scss
.heading-2 {
  @apply text-black text-24 bold;
}
<h2 className="heading-2 mb-4">Hello world!</h2>

This might not be what you want in every situation — maybe you do want every heading to have a certain margin. But for many cases it’s a trick worth keeping in mind that makes your components much more flexible and less dependent on their position on the page.

7. Treat tailwind.config.js as your source of truth

In SCSS or LESS, you might create variables for constants such as colours, fonts and max widths. You can reuse those variables anywhere in your CSS, and if you change the variable this change will be reflected everywhere it’s used.

Tailwind works in much the same way, except everything is defined by variables. That means not just the text or background colours you can use, but also spacing, sizing, borders and almost any other property you can think of. You can make use of this with the theme object in tailwind.config.js, or extend the default theme with the extend object.

This file defined the way your whole app will look: If your designer has used a design system which commonly uses numbers like 4, 8, 12, 16, 32 etc. you can build that system right into your CSS:

spacing: {
  1: '4px',
  2: '8px',
  3: '12px',
  4: '16px',
}

These classes are then ready to use, straight away, and in the case of the spacing property will be applied to padding, margin and relative positioning classes such as left and right.

Don’t forget that you can also use regular JavaScript to generate some of these properties, which can save a bit of time and clean the file up. I like to make a const containing an array similar to the one above, and using it for spacing, width, height and any other similar properties – even font size.

I’ve also entertained the idea of such a spacing/font system generated using the golden ratio, which might be a great option for fast prototyping while also maintaining a great visual flow.

8. Use tools to spot repeating patterns

On the topic of composing classes, there are some great tools to help you find repeating patterns in your class lists so you can refactor these into their own generic classes.

One of the most useful is Refactor CSS, a VS Code extension that automatically finds and presents very similar strings of classes, which helps when finding common patterns to abstract into new classes. Class strings will be highlighted if they have more than 3 classes and these 3 classes repeat more than 3 times in the current document. The order of classes is ignored, so you don’t need to worry about maintaining a property sort order to make sure the tool works.

If you are worried about property sort order (Tailwind is much more readable if you are) then you can use another tool to deal with that for you: Headwind. This VS Code extension will format your Tailwind classes on save and group them by their function, making sure everything is where you expect it.

// before saving
<div class="bg-red container mb-6 text-white"></div>
// after saving
<div class="container mb-6 text-white bg-red"></div>

You can also change the regex of Headwind, so you can customise the sort order exactly how you like.

Conclusion

I’ll won’t pretend moving to this way of working is particularly easy, and there are many problems that still need solving. We’re still in the early (ish) days of utility-first frameworks and component-based development, so everything hasn’t been figured out yet.

Despite this, I feel confident that we’ll start to see new tools, packages and methodologies that aim to tackle any issues we may face. What we can get out of this approach are lightweight, fast applications that are truly DRY. Isolated, independent components are great for building across different platforms, so I think we’ll see a lot of companies building headless systems picking up this way of working.

I write more articles like this one about headless systems and component-based development over on my blog at npm run dev. Check it out if you want, and I’d love to get some feedback on my thoughts and writing style. Thanks for reading!


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *