CSS is giving us a headache. Maybe Tailwind CSS is the cure?

Kevin Simons
August 26, 2020

Let's face it - sometimes CSS can be a pig. You need to learn the magic incantations, carefully follow the phases of the moon, and pray that everything will be ok if you just try every possible permutation of the flexbox properties.

But that's not what this post is about. This post isn't about the pain that can come from getting CSS do your bidding, this post is about the pain that comes from trying to scale CSS in a large project. This is the first of a series of posts as we search for the solution that allows our developers to spend more time working on new features and less time writing CSS.

We'll take a look at Tailwind CSS and see if it fits the bill.

Spoiler alert: it's easy to get up and running with Tailwind and it feels highly productive. But it's too soon to tell if we'll go all-in with it. A follow-up is needed to investigate Tailwind's facilities for responsive design and accessibility.

A bit of background

We're building Kitemaker, a new, fast, and highly collaborative alternative to issue trackers like Jira, Trello, and Clubhouse. We're helping teams build better products and have a bigger impact through closer collaboration. It's a pretty big React application, and from day one we've used SASS for our CSS needs. We use CSS modules. We haven't used any off-the-shelf component toolkit (some regrets about that). We support dark mode (awesome!) by making heavy use of CSS custom properties. We took a cue from Notion and use system fonts everywhere. We have some @mixin directives for repeated patterns in our UI and occasionally make use of SASS' @extends facilities (though we always suspect we're doing it wrong when we do).

Board view in Kitemaker.

Sounds ok. So what's the problem?

The problem is our CSS has become a mess over time. We have tons of classes. Some of them are very specific to the use case in a particular piece of the UI, some are written to be more general and reusable. Sometimes a class isn't quite right for a use case, so it gets duplicated and edited. Sometimes a mixin isn't quite right for the use case so it gets another parameter added to it. Sometimes a new custom property slips in and we get inconsistent colors or borders. We have a lot of SASS variables to handle spacing (padding/margins) and it's not always obvious which one to pick. Sometimes a developer innocently changes a color here, or a custom property there, fixing their use case but breaking another. We have a hierarchy of CSS classes, but those can be brittle and a challenge for reuse, and in some cases a big surprise where you need to open web inspector and ask "where did this color come from?".

The net result is that we spend too much time tweaking CSS and rarely is the codebase left in a better condition than it was found.

Now, the pundits may say that it's simply because we're not good enough or disciplined enough at CSS - that we don't apply the same rigor to CSS that we apply to the TypeScript and React code that houses it. That may be. We're probably never going to be masters that make amazing works of art with CSS (you should seriously check out @liatrisbian or @jh3yy if that's what you're after). But here's the thing - we don't think we should need to be. We think it should be able to crank out UIs at a reasonable pace without generating a pile of CSS debt in the process.

Criteria

What, exactly are we looking to get out of a CSS framework?

  • Something that makes it easy to be consistent in terms of things like margins, padding, colors and sizes.
  • Something that's customizable enough for our needs
  • Something that works with our home grown components
  • Something that doesn't make it harder to support dark mode
  • Something that helps us make our app more responsive. We were just about to go through all of our app and make it work better on smaller screens, so something that'll kick start the process would be a massive win
  • Something that helps (or at least doesn't hinder) accessibility
  • Something that doesn't make our build process even more cumbersome
  • And last, but not least - something that feels right. We want to come out of this feeling more productive than we are going in when it comes to developing UIs.

Let's see if Tailwind CSS is what we're looking for or not!

Tailwind CSS

Ok, so what is Tailwind CSS? It's a CSS framework that allows you to compose low-level utility classes together right in your HTML (or JSX). It's the rising star among the frameworks that have been referred to as atomic CSS (or functional CSS).

What do these low-level classes look like? Well, they're things like:

  • font-bold - add bold font
  • text-gray-600 - gray text (the 600 represents the darkness of the gray, ranging from 100 to 900)
  • md:flex - set display: flex on medium sized screens (the md: prefix is part of Tailwind's responsiveness support)
  • hover:underline - underline the contents on hover

Everything from colors, to flexbox and grid, to background images. You take these things and compose them together to get the look you're after. So if you want your button to be blue, have a bit of padding, have rounded corners, have bold white text, and to change to a darker blue on hover, you'd write something like this:

<button class="bg-blue-400 p-2 rounded text-white font-bold hover:bg-blue-600">Click me</button>

That's it. No opinions on what a button (or any other component) needs to look like, how your grid system works or anything like that.

Now of course, typing out all of those things can get a bit repetitive, so you can extract things into higher order component classes like this:

<button class="btn btn-blue">Click me</button>

<style>
  .btn {
    @apply font-bold p-2 rounded;
  }
  .btn-blue {
    @apply bg-blue-400 text-white font-bold;
  }
  .btn-blue:hover {
    @apply bg-blue-600;
  }
</style>

You can also extend it with your own screen sizes, colors and more. It's pretty neat!

Now, a lot of people's first instinct (and I'm no different) is that this looks like it's going to be at least as messy as the situation we're coming from (if not more). However, there's a real glimmer of hope and that's that things are extremely explicit. The classes you add are the ones you get and if you're careful to not overdo trying to make everything into components, things just might work out. Let's give it a go, shall we?

Kicking the tires

We're going to put Tailwind CSS to the test by reimplementing a piece of our app. And what better piece than the very first one users are greeted with - the login screen.

Login screen in Kitemaker.

We were going to test it by making a mock project and trying to reproduce the screen there, but then we thought - eh, let's live a little. We'll make the changes right in the real codebase. Let's just start by deleting all of the className properties from the existing code. The result:

Somewhat less good.

We've got a bit of work to do! We won't go through every step it took to build things back up to their former glory, but let's check out some of the highlights.

Installing Tailwind

Getting Tailwind up and running in our existing webpack setup was surprisingly easy:

yarn add -D postcss-loader tailwindcss
npx tailwindcss init

Then a little modification to our CSS rule in webpack.config.js:

const cssRule = {
  test: /\.css$/i,
  use: [
    ...
    {
      loader: 'postcss-loader',
      options: {
        ident: 'postcss',
        plugins: [require('tailwindcss'), require('autoprefixer')],
      },
    },
		...
  ],
};

Rather than muck with our existing SASS files (and all the CSS module cruft that came along with them), we'll just add a brand new CSS file containing:

@tailwind base;
@tailwind components;
@tailwind utilities;

Add a <link> for this new file in our HTML entry point and that was it. Pretty awesome!

Do yourself a favor - install the IntelliSense plugin

If you're using VS Code, get this thing immediately:

Especially when you're first learning, it makes exploring what's available much easier.

Start with something easy - buttons

Styling our button component was pretty easy indeed. Our button component has an intent property that controls the button's appearance (e.g. primary, secondary, danger, etc.). So wiring it up with Tailwind looks like:

const intentStyles = {
  default: '',
  primary: 'bg-blue-500 active:bg-blue-700 hover:bg-blue-300 text-white',
  secondary: 'text-blue-500 bg-none border border-blue-500 active:bg-blue-700 hover:bg-blue-300',
  danger: 'bg-red-500 active:bg-red-700 hover:bg-red-300 text-white'
};

return (
  <button
    className={cn('rounded-sm py-2 px-3 border-0', intentStyles[intent ?? 'default'])}
    ...
  >
    ...
  </button>
);

With that in place, our button starts to look pretty button-ish!

This was all straightforward - basically just reproducing our existing CSS with Tailwind's utility classes. Needed to check the documentation fairly frequently, but that's fine as it's well structured and readable.

At first glance, developing this way looks (and feels) a bit verbose and awkward. We've always been taught to try to keep our presentation separate from our logic. This will take some getting used to! However, it's great that there are no surprises, no hierarchies to worry about and no thinking about "what's a good name for this class?". Let's push on a bit further.

But those aren't the right colors

Yeah, so we cheated a bit and just used bg-blue-500 and friends. Not what we really want. Our old SASS code had things like this:

color: var(--accent-color);

That's a CSS custom property (variable) which we rely on to make dark mode work. How are we going to get Tailwind to spit that out for us?

Turns out it pretty easy. First we need to modify our tailwind.config.js file to tell Tailwind about our custom color extensions:

module.exports = {
  ...
  theme: {
    extend: {
      colors: {
        accent: {
          default: 'var(--accent-color)',
          hover: 'var(--accent-color-hover)',
          pressed: 'var(--accent-color-pressed)',
        },
        danger: {
          default: 'var(--danger-color)',
          hover: 'var(--danger-color-hover)',
          pressed: 'var(--danger-color-pressed)',
        },
      },
    },
  },
  ...
};

As you can see, it's easy enough to add the CSS variable names.

Then back in our React code we can use classes like bg-accent (which corresponds to the default above), as well as bg-accent-pressed, etc:

const intentStyles = {
  default: '',
  primary: 'bg-accent active:bg-accent-pressed hover:bg-accent-hover text-white',
  secondary: 'text-accent bg-none border border-accent active:bg-accent-pressed hover:bg-accent-hover',
  danger: 'bg-red-500 active:bg-red-700 hover:bg-red-300 text-white'
};

return (
  <button
    className={cn('rounded-sm py-2 px-3 border-0', intentStyles[intent ?? 'default'])}
    ...
  >
    ...
  </button>
);

Now our UI will keep responding to changes in CSS dynamic properties the way it did before, for example when switching to dark mode. Nice!

Flexing

They layout for this page is a pretty simple flexbox set up with a lot of centering involved. This is one spot where Tailwind was clearly better than our previous solution. We use flexbox everywhere in Kitemaker, and so we made various SASS mixins for various configurations of flex rows and columns (with parameters for align-items, justify-content, etc.). In Tailwind it's as easy as:

<div className="flex flex-col items-center justify-between min-h-screen"></div>

We want:

  • display: flex
  • flex-direction: col
  • align-items: center
  • justify-content: between

They even have a handy class min-h-screen which sets min-height: 100vh.

Spacing

The spacing classes for Tailwind are really nice and easier to remember than the shortcut counterparts in vanilla CSS. We never seem to remember which order things like padding: 0px 10px 14px 10px are in, but Tailwind's classes are clear and explicit:

  • mx-1: set the margin in the x direction, size 1 (.25rem)
  • mt-4: set the top margin
  • p-1: set padding in all directions
  • space-x-1: set spacing between children in the x direction
  • Etc

There are still a ton of options for spacing sizes though, so it's not immediately obvious that this will lead to increased consistency when it comes to the use of spacing in our application.

Wrapping it up

In the end, we spent a few hours and ended up pretty darn close to where we started, including a dark mode that still works:

Considering we'd never touched the framework before, we're pretty happy with those results.

Where'd we end up in terms of our criteria?

  • Does Tailwind make it easy to be consistent in terms of things like margins, padding, colors and sizes? Definitely does in terms of colors. Being forced to define the theme in the Tailwind configuration forces you to make conscious choices about colors. Spacing, on the other hand is not as clear - it's still easy to shoot yourself in the foot by picking the wrong spacing primitives. Further work is needed to see if some higher level components can help steer us towards consistent spacing.
  • Is Tailwind customizable enough for our needs? So far! Making our own color palette, including CSS custom properties was a snap.
  • Does it work with our home grown components? Yup! No opinions on components here.
  • Does it make it harder to support dark mode? Nope! CSS custom properties work just fine and that's all we rely on for our dark mode support.
  • Will it help us make our app more responsive? Not sure yet. We need to do a follow up with a page that's problematic on smaller screens and really dig into this. The screen size prefixes (sm:, md:, etc.) are very promising.
  • Will it help us make our app more accessible? Not sure yet. Tailwind has some features for accessibility. Also, it uses rem units everywhere, which some argue is an accessibility benefit. Further investigation is needed here.
  • Does it make our build process even more cumbersome? Just a small tweak to our webpack setup. And ultimately we'd be able to drop our SASS/CSS modules setup, which would be a net win.
  • Does it feel right? It's too early to say, but it's very promising. It feels very explicit. You know that the blast radius of the changes you're are making is very small. There's also no searching for the right classes once you have the Tailwind classes in your muscle memory. There's some questions about whether sweeping changes to the UI (let's change the spacing everywhere!) would be straightforward, but let's be honest - when have they ever been?

Thanks for reading! Did you find this article useful? If you want to see more material like this follow @ksimons and @KitemakerHQ on Twitter.