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.
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).
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.
What, exactly are we looking to get out of a CSS framework?
Let’s see if Tailwind CSS is what we’re looking for or not!
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:
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:
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:
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?
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.
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:
We’ve got a bit of work to do! We won’t go through ever step it took to build things back up to their former glory, but let’s check out some of the highlights.
Getting Tailwind up and running in our existing webpack setup was surprisingly easy:
Then a little modification to our CSS rule in webpack.config.js:
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:
Add a <link> for this new file in our HTML entry point and that was it. Pretty awesome!
If you’re using VS Code, get this thing immediately:
Especially when you’re first learning, it makes exploring what’s available much easier.
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:
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, I like 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.
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:
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:
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:
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!
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:
They even have a handy class min-h-screen which sets min-height: 100vh.
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:
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.
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?
Kevin Simons is the CTO of Kitemaker, the super-fast issue tracker that makes teams better, faster, and happier
A particular form of procrastination I've found in others and me — failure to just get started.
In this article we describe the background for why we started building Kitemaker.