Sailing the C(SS) is Easier When You Have a Tailwind

Posted on
A paper boat floating in water

Or: How I Learned to Stop Worrying and Love Utility-First CSS Frameworks

If you go back in the history of web development far enough, everything was an inline style. Then CSS came along and brought a decoupled and powerful way of styling your site, and a lot of headaches for web developers who weren't clear on how to float with the tide, to overflow a metaphor. But if, like me, you've loved crafting elegant Sass mix-ins and having nicely named BEM identifiers, you may also wonder why we'd give up so many of the advantages that CSS has brought us and use a utility-first CSS framework like TailwindCSS.

But first, what do we mean by "utility-first"? You may have used a utility class before, either one you wrote yourself to accomplish a simple but repeatable styling task, or in a framework like Bootstrap. This is typically one or more style properties that are used globally, outside of any individual component, to do something you'd otherwise have to repeat a lot of styles for. For example, a class that makes text visibly hidden, but which can be found by screen readers.

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

 

That would be a lot to repeat in several of your BEM-style components, so it's handy to have some global CSS classes that take care of it. A utility-first CSS framework tries to provide that DRY (don't repeat yourself) convenience to all your CSS styles. In the case of Tailwind, nearly every CSS property you can think of writing has a pre-defined class ready to go. If you want to see it for yourself, go ahead and scroll through Tailwind's docs, I'll wait.

Goodbye, Cascade!

There are two ways of using Tailwind's utility classes in your project. The first is to apply them to your normal CSS, with Tailwind's handy @apply directive. This keyword tells your CSS preprocessor to find Tailwind's defined class and copy it into the class you're creating. For example, let's say we're creating a card component and want to apply styles from Tailwind.

Filename
card.css
.Card {

    @apply bg-tan/75 border mx-4 rounded w-1/3;

    /* Output:
    background-color: rgba(210, 180, 140, .75);
    border: 1px solid black;
    border-radius: .25rem;
    margin-left: 1rem;
    margin-right: 1rem;
    width: 33.3333% */
}

 

Okay, that's a bit more concise, but what of it? Where's the advantage from writing CSS in a slightly more terse format? For one thing, reusable utility classes help us keep consistency. The color of that tan background, or the pixel radius we round off the borders is the same everywhere we use it, which is something we'd previously use Sass or CSS variables to ensure. But more on that later.

But can we do more to streamline this? Do we need to be adding another stylesheet just to apply these styles to our card component? Surely, if we're making a card component, we're not just reusing the CSS. We should be reusing the HTML as well, since any modern templating language that works with components should allow us to build out all the logic of our card in one place.

Filename
card.jsx
const cardClasses = "Card bg-tan/75 border mx-4 rounded w-1/3"

const Card = (
    <div className={ cardClasses }>
        My Card Body
    </div>
);

 

But wait, we just did a couple things here that break some of the fundamental rules of CSS! First, by using classes that describe what the page looks like instead of the function of the element, we're tying styling back to our content! Well... sort of. Web development's changed a lot since that rule came about. We're not often making entire documents that contain our entire HTML structure and repeating it across several documents. In this case, our card is going to look the same wherever we use it, regardless of if it's getting its styles from the .Card class or directly in the HTML. The content for the card is coming from either an API or another component that passes the real content in as props. If our card has conditional modifiers, we can add the classes right into the logic that applies the modifiers, instead of abstracting them out through another class. So, looking at it this way, our card template really is still separating the styles from the content. But now the styles also contain the structure and presentational logic, in one portable file. It's like CSS in JS, without the mess that brings.

Okay, but still. By making all of our styles "inline", we removed the cascade from CSS! How do we manage specificity? Well, specificity hasn't really gone away. If I set text-color-red on one element, it'll still propagate down to all its children. If I want to override that, I can set another text color on a child further down the line. It may still be helpful to set some base element styles at the end of your tailwind.css file so that all your <a> tags get the same base color without making templates at the atomic level, but you can still override an element-level style declaration with a class-level declaration when you get to having a specific <a> tag that you want to style differently. In fact, you'll probably find that you run into a lot fewer specificity issues by writing all your CSS this way, since you'll see every class that an element should have on it or inherited from its ancestors.

Keeping It All Inline

So, having looked at the classes Tailwind provides out of the box and the level of customization available, we have a pretty good idea of how Tailwind replaces some of the things that Sass or CSS variables could do to make sure that our websites have consistent color schemes, spacing, text sizes, and so on. But that's only the beginning. The tailwind.js file stores a JSON object for all the customizing we want to do on the set of classes Tailwind offers out of the box. But it's also a JavaScript file that runs when the CSS is compiled, so we can do a lot more in there if we want to.

For example, I want to make my color declarations as easy to reuse as possible. That might include passing a certain shade into another custom class that Tailwind doesn't generate, like text-shadow. Or, I want to use the same shade, but generate a different opacity for it. Here's a combination of CSS variables and a little function to allow me to pass in a color value in a number of formats and get back a usable color for my Tailwind config.

Filename
colors.css
root {
  --color-white: 255, 255, 255;
  --color-black: 0, 0, 0;
  --color-black-50: 0, 0, 0, .5;

  --color-red: 249, 126, 114;
  --color-orange: 209, 134, 22;
  --color-yellow: 254, 222, 93;
}

 

Filename
tailwind.config.js
'use strict';

const colorFn = cssVariable => {
  return ({ opacityVariable, opacityValue }) => {
    if (opacityValue !== undefined) {
      return `rgba(var(--${cssVariable}), ${opacityValue})`;
    }
    if (opacityVariable !== undefined) {
      return `rgba(var(--${cssVariable}), var(${opacityVariable}, 1))`;
    }
    return `rgb(var(--${cssVariable}))`;
  };
};

module.exports = {
  theme: {
    colors: {
      black: colorFn('color-black'),
      white: colorFn('color-white'),
      gray: colorFn('color-black-50'),
      red: colorFn('color-red'),
      orange: colorFn('color-orange'),
      yellow: colorFn('color-yellow'),
    }
  }
};

 

You could further customize this to give you automatically generated light and dark shades of your base colors, using a PostCSS plugin such as PostCSS color-mod(), similar to Sass' lighten() and darken() functions.

Having consistency with responsive breakpoints is another nice thing to have, if you want to customize Tailwind's default set of breakpoints, but also need to use a media query in some other bit of styling. Unfortunately, CSS variables don't work within media query declarations, at least not until CSS environment variables hit. There is a PostCSS plugin for this, though. Just put your media query size variables in a root declaration, and then use them in your Tailwind config or anywhere else.

I also can't say enough for Tailwind's incredible Just-In-Time mode, which should be enabled by default when Tailwind 3.0 releases. This removes a lot of the configuration needed to enable certain variants or add new classes for one-off use cases, by intelligently scanning your CSS or template files for class names and building them at compile time, making sure to only include classes that you're actually using in your distributed .css file. And it does this in a fraction of a second. It's really impressive.

Sail Away

However you write your CSS, we should accept that the doctrine up until now won't be here forever. There are a lot more options for styling, some of which look even more alien from the CSS we're used to. Tailwind has been pretty quick to adapt to new CSS features and needs while making their syntax less obtuse. Would I say Tailwind is a good library for people who are struggling with CSS? No. If anything, Tailwind requires just as much, if not more knowledge of CSS fundamentals than vanilla CSS does. But for those who still find, after all the years of debating new design methodologies and style guides, that untangling the CSS that a full and busy development team has left you with shouldn't be as hard as it is, Tailwind provides a fresh breeze of new perspective.

Photo by Artak Petrosyan