Tailwind CSS for the Experienced Flyer

Ben Tinsley, Former Front-End Developer

Article Categories: #Code, #Front-end Engineering

Posted on

A few tips for you to get the most out of your Tailwind experience.

When Jeremy Frank first told me about Tailwind CSS, I thought something along the lines of "Disgusting, why would anyone want to inline their styles?" But after I saw how he had applied it on a project and had good things to say about it, I was curious. I had so many questions:

Maybe the way I've been writing CSS could be improved? Maybe cascading the styles through classes wasn't the best approach? Or, maybe I could learn a new approach in trying it out for myself?

And that I did. I recently built an interactive report for one of our clients and decided to use Tailwind to discover its strengths or weaknesses for myself.

After the experience, I can say with certainty that I will use Tailwind again. It was a great experience and I recommend you try it out, too, if you haven't already. There are a couple of tips I picked up from the experience that I’d like to share with you to ensure you get the most out of your Tailwinding.

1. Turn off all modules by default

The default configuration file that you’ll generate from the initialization script provides a great way to get going and build out a quick and easy site or application without having to think too much. In fact, if you want to use Tailwind as more of a Bootstrap alternative, ignore this point and move on to #2 below.

Out of the box, Tailwind has exactly what you need to build a robust project without wasting time writing CSS or thinking about a style system. However, if you’re doing something totally custom, the first thing I recommend is setting all of your modules to false in your tailwind.config.

Why do this? Simple. Tailwind generates classes for each of your defined modules, so setting everything to false ensures that when it's time to use a module, you include it in a way that makes sense to your project. You don't need a bunch of unused classes generated with your project. To look at a specific example, Tailwind, by default adds shadow classes for responsive, hover, and focus variants. I don't use box shadows on every project I do, so leaving this as-is is just a waste.

As an added bonus, clear out all the values from each of the particular modules, or at least the ones that don't immediately seem applicable. This way, you’ll ensure you are adding values that apply specifically to your project. If working with a designer, she or he may have a different method of spacing items than what is defined in the margin or padding config. Therefore, it may save you more time to just start from a clean slate than add to what is already there.

And as a final check to ensure you are delivering only critical utility classes, use PurgeCSS as the Tailwind team recommends in their documentation. PurgeCSS will strip out all the unused CSS classes in the final build so if you get a little lazy with your config or you are hesitant to remove a property altogether, you don’t have to worry about any bloat.

All of the work suggested in this point will absolutely make the initial build of the project you’re working on take more time, but once more and more values are in place, you’ll rest easily knowing you’re delivering the most performant version of Tailwind classes you can with as little bloat as possible.

To make it easy for you, I’ve created a Stripped Version of the Tailwind config that you can feel free to use on your project. If you do use it, let me know in the comments!

2. Label your divs

In my experience with Tailwind, I used it in conjunction with React, which made it incredibly easy to generate the appropriate styles based off the content passed into the component.

Take this StyledText component as an example:

import React from 'react'
import cx from 'classnames'

class StyledText extends React.Component {
  generateFontClasses() {
    const { width, textSize } = this.props
    const isComponentFullWidth = width === 'full'
    const hasCustomTextSize = !!textSize

    return cx('font-sans leading-spacey', {
      'text-22': isComponentFullWidth && !hasCustomTextSize,
      'text-16': !isComponentFullWidth && !hasCustomTextSize,
      [`text-${textSize}`]: hasCustomTextSize ? textSize : false
    })
  }

  render() {
    const { text } = this.props.content

    return (
      <p className={this.generateFontClasses()}>
        { text }
      </p>
    )
  }
}

export default StyledText

In this, I generate a <p> tag. This calls this.generateFontClasses() which uses the classnames module to determine if the text should be larger or smaller than the default based off its width or textSize props. If the width should be full, the text is larger, or if textSize is defined as a prop, it’ll take whatever value is passed in that way.

In all, this is a pretty easy way to ensure the text size looks right for the layout of the item on page. However, you may see where this poses a problem. If I come back to this codebase later and need to make a change to this, but I’m only looking at the front end in the browser, I'm going to have a bad time. If looking at it in the browser, it’ll render it like so:

<p class="font-sans leading-spacey text-22">Some Text Is Here</p>

Let's say the leading looked wrong or the font should actually be serif, how should I go about tracking this component down in the codebase? I could do a search for font-sans leading-spacey text-22 but that will return no results, because that class name string is dynamically generated. I could search for font-sans or leading-spacey but I’ll waste time tracking down every other element on the project with those same classes and maybe even be unsure once I do find the correct component.

Tailwind and other utility class approaches bring up an interesting issue with the way the class attribute has been used over the past few years. In the past, I’d use my classes to name my element or component, but using a utility class framework like Tailwind, that method is not valuable. So writing something like:

<p className="styled-text [tailwind utilities...]">...</p>

feels weird especially since styled-text may not even exist as a class in your CSS. What to do?

Data attributes, the wind beneath your wings

What I’ve found incredibly helpful to use is data-label as a way to identify your elements. This way, you’ll be able to relegate the class attribute to just Tailwind utilities and the specific data attribute to ensure anyone looking at your code knows how to find the element in your codebase and what it’s supposed to be.

To fix that code example from above, simply adding a data-label attribute will solve so much heartache when it's time to maintain your project:

render() {
  const { text } = this.props.content

  return (
    <p
      data-label="styled-text"
      className={this.generateFontClasses()}
    >
      { text }
    </p>
  )
}

This way, when you look at it in the browser it’ll be something you can track down easily in your code:

<p data-label="styled-text" class="font-sans leading-spacey text-22">Some Text Is Here</p>

Don't feel like you should put them on every element. Only on top level elements per file, or ones that seem complicated enough to warrant it. Use your noggin.

3. Only use @apply as a last means

This last one is an opinion for sure, but this is an entire post on my opinions for making Tailwind, so buckle up. Tailwind has a handy feature which allows developers to apply utility classes directly into their CSS.

So instead of writing:

.heading-text {
  font-size: 2rem;
  margin-bottom: 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid #333;
}

You could write:

.heading-text {
  @apply text-32;
  @apply mb-24;
  @apply pb-16;
  @apply border-b-1;
}

Seems handy right?

My issue with this is it obviates a feature of Tailwind that is the best feature of all - not having to resort to a CSS file to style your work.

With more traditional approaches to CSS and layout, you’d have your markup in an html or twig or php or jsx file, your styles in a css directory, and then flip between the two as you style out your work.

If you’re used to doing things this way, you’ve probably encountered a few issues. For one, you probably had trouble at times naming elements to target, often having to come up with what felt like creative hacks or alternate naming schemes to get the job done.

Another issue you’ve probably encountered is problems with CSS specificity. This happens in one of two ways: 1) Either some CSS rule is too specific and it forces you to be overly specific in other places to ensure the styles get applied as intended, or 2) the specificity isn’t targeting the element you want it to, which wastes time as you try to debug the abstracted DOM into CSS nested classes.

These suck! They’re a big time waste or ultimately make you feel less happy about your work when you have to work around them. Tailwind offers you an escape rope from ever dealing with these problems by allowing you to do all the styles in your markup, never having to deal with naming conundrums or specificity collisions again.

If you’re writing your markup in a language that allows logic, you can use that to determine the type of content passed in and display the content with the appropriate styles applied.

Take a look at my StyledText component from above, listed again here for reference:

import React from 'react'
import cx from 'classnames'

class StyledText extends React.Component {
  generateFontClasses() {
    const { width, textSize } = this.props
    const isComponentFullWidth = width === 'full'
    const hasCustomTextSize = !!textSize

    return cx('font-sans leading-spacey', {
      'text-22': isComponentFullWidth && !hasCustomTextSize,
      'text-16': !isComponentFullWidth && !hasCustomTextSize,
      [`text-${textSize}`]: hasCustomTextSize ? textSize : false
    })
  }

  render() {
    const { text } = this.props.content

    return (
      <p
        data-label="styled-text"
        className={this.generateFontClasses()}>
        { text }
      </p>
    )
  }
}

export default StyledText

It has certain metadata associated with it through props, like width and textSize. With just these two, I’m allowing the content to determine what it should look like and letting the utility classes do the work, instead of applying class="full-width-text" or something equally heavy-handed to the element.

Make your components small and aerodynamic, though

This forces me to think differently about my markup, though. I need to isolate each extractable element into its own component, so it's not muddled and confused with another one. Anything that could be taken as StyledText in my project should use that component, instead of doing something inline. This will ensure my project is maintainable and any changes that need to happen can be done universally, instead of in a case by case basis, which was such a pain to build and maintain way back when inline styles were the only thing to do.

So to show you a hypothetical parent component for the StyledText, it would look something like this:

// pseudocode warning!!!
// copy/paste at your own risk!!!
import React from 'react'
import Container from './layouts/container'
import StyledHeading from './typography/styled-heading'
import StyledText from './typography/styled-text'

class Article extends React.Component {
  render() {
    return (
      <Container
        containerPadding={this.props.containerPadding}
        containerWidth={this.props.containerWidth}
      >
        <StyledHeading
          element={this.props.headingElement}
          headingSize={this.props.headingSize}
          headingOptions={this.props.headingOptions}
          text={this.props.headingText}
        />

        { this.props.paragraphs.map((index, paragraph) => (
          <StyledText
            text={paragraph.text}
            textSize={index === 0 ? 'text-24' : false}
            width={index === 0 ? 'full' : 'half'}
          />
        ))}
      </Container>
    )
  }
}

export default Article

In this example, you can see each component rendered from this Article has metadata tied to it through props. Similarly to the StyledText component, each component will render Tailwind utility classes conditionally based off their given props. So if I have several different types of containers, with different widths or paddings, I can bypass having to create CSS classes to style them and simply generate the appropriate Tailwind utility classes on the component. Likewise, the StyledHeading may know it should be rendered as an <h2> or <h3> based off the element passed to it, and render different styles on the component level.

Again, doing it this way may present a challenge to the way you’ve built your projects in the past. But if you are bringing in a library to write CSS for you, avoiding CSS altogether is the optimal way to go in my book. Obviously, some things will have to be done in CSS, but be sure to check for a Tailwind plugin first, or build your own.

In Conclusion

If you’re like me, you may have thought Tailwind was a bad idea until you tried it and had that eureka! moment. But don't use Tailwind like a bozo! Ensure you’re not adding unnecessary bloat to your project by starting with a clean slate in your Tailwind configuration file. Ensure you’re setting yourself and others up for success when it’s time to maintain your project by applying data-label to your parent divs. And finally, ensure you aren't making the same dumb mistakes with CSS while using Tailwind and try to go all in with the inline utility classes. It may be uncomfortable at first, but once you get the hang of it, writing CSS will seem archaic and cumbersome to you.

Any thoughts? Maybe you see a huge glaring hole in my approach? Were the airplane metaphors too sparse or just missing the mark completely? Hit me up in the comments, I'd love to hear from you!

Related Articles