A leafy tree is buffeted by a strong wind. In the foggy background of the windswept landscape, a second, nearly identical tree can barely be seen.
Photo by Khamkéo Vilaysing on Unsplash

CSS and its language of selectors to match patterns of DOM elements were designed to solve a different problem than we face in our web application codebases at Culture Amp.

If you need to provide a stylesheet for a site full of documents that will grow and evolve independently of your styles, where the content will vary from document to document even as the styles need to remain consistent between them, then a set of CSS selectors that describe the consistent style rules for those independently written documents is exactly what you need. Tailwind CSS is a poor tool for this job, as it would see you repeating the same groups of class names to apply consistent styles to elements in your HTML.

On the other hand, if you are building a web application whose graphical user interface is already composed of JavaScript components (e.g. React), then you are already eliminating duplication in your HTML with those components. Tailwind is the perfect tool for this job because it saves you from having to maintain a set of CSS styles in parallel to your JavaScript components, tightly coupled to the HTML generated by those components.

While it may feel tidy and efficient to keep your UI styles separated from your UI components, in practice, most changes to UI components will need corresponding changes to their styles, and vice versa. So, all you’re doing by maintaining tightly coupled code in two places is creating unnecessary work for yourself – and for the browser, which has to parse and apply much more CSS, as each of your components ends up with its own style rules, even when its styles aren’t actually that unique.

The Decision to Adopt Tailwind at Culture Amp

Permalink to The Decision to Adopt Tailwind at Culture Amp

I originally wrote the rationale below as an internal explanation for our decision to adopt Tailwind CSS at Culture Amp in mid-2022.

At the time, we had been writing CSS using Sass and compiling it with the venerable (and still excellent) CSS Modules. We had reached various painful symptoms of scale with this approach, namely stylesheet bundle size, build performance and developer experience, and it was time to consider other options. The two we were considering were Vanilla Extract, which is essentially a more modern and feature-rich CSS Modules from our friends at SEEK, and Tailwind CSS – a controversial and potentially divisive technology that flies in the face of decades of CSS dogma, ostensibly to provide much greater developer productivity and dramatically smaller CSS bundles.

For these reasons, as of mid-2022, we had decided to try Tailwind CSS as an experiment at Culture Amp. That experiment was successful, and Tailwind CSS is now the standard way to add custom styles to user interfaces at Culture Amp, but here’s what I wrote at the time:

The promise of Tailwind CSS is that it largely eliminates the need to write new CSS, which will not only solve our ever-growing bundle sizes and build times (some of our main drivers for moving off of Sass CSS Modules), but may vastly reduce cognitive load and improve developer experience (DX) for engineers building UIs. They will no longer need to devise a meaningful class name for every element and variant, and work across two files to wire styles up to their HTML.

Instead of needing to come up with a meaningful class name (toolbar):

<div className={styles.toolbar}>

…and then add this to your stylesheet:

.toolbar {
  display: flex;
}

…Tailwind CSS just provides you a predetermined class name to do this, and just about everything else you need to do with CSS:

<div className="flex">

Tailwind CSS knows what flex means, and generates the necessary CSS for you, and it does this just once to cover every instance of display: flex in your entire app! Instead of a single class name that applies multiple style properties to an element, you add multiple class names to your element, about one per CSS property you want to set. And you never need to open a separate file for your styles.

Tailwind CSS’s core idea is that 99% of the CSS we write today is unnecessary repetition, that would be better expressed with a higher-level language inline with our HTML, tight coupling with which is inevitable. Even if you like CSS and enjoy writing and maintaining it [which I do –Kev], Tailwind argues that this is usually not a good use of our time.

This is a controversial idea, because roughly half our engineers quite like CSS, or at least are invested in and proud of their skills in this area. Not only have we been taught to avoid inline styles, and separate the concerns of content and presentation, but Tailwind CSS’s language of abbreviated class names composed into long strings is alien to us, and runs against every instinct we have for code readability.

But if Tailwind CSS’s theory proves true in practice for Culture Amp engineers (and the experience of Campers who have used it before suggests that it will), Tailwind CSS could have a far greater positive impact on our productivity and our code’s maintainability than any alternative we are aware of. The potential win is big enough that we owe Tailwind a shot.

Recommended viewing: A Real-Life Journey Into the Opinionated World of “Utility-First” CSS with Simon Vrachliotis

Recommended reading: From semantic CSS to Tailwind - Refactoring the Netlify UI codebase

Nothing comes for free, of course. Tailwind CSS effectively introduces a new compiler (and associated editor integrations for autocomplete, formatting, and linting) to our toolchain, which recognises the class names we use and thereby generates only the necessary CSS output. Tailwind therefore requires us to be familiar with and mindful of the limits of one more consumer of our source code (e.g. Tailwind cannot spot dynamically constructed class names). The required mental model is much simpler than the one we’ve needed for Sass CSS Modules, but is not as simple as the “it’s just TypeScript” experience offered by Vanilla Extract.

Also, while Tailwind CSS vastly reduces the cost of “common” styles, it takes some tools away from us for writing maintainable custom styles. There is no facility, for example, to scope styles to a single component (e.g. hashed class names from CSS Modules, or prefixed class names from frameworks like Vue). Tailwind’s documentation uses the BEM naming convention for this, which we know from experience does not scale well in a crowded codebase. And assuming you want to put your component-specific styles in a CSS file alongside your component, you must add an @import for it to a central CSS file (you can’t just import it from your component). In general, the ergonomics for writing custom CSS are significantly worse, but Tailwind CSS promises the need to write such code becomes extremely rare. And PostCSS today does give us some facilities that we used to need Sass for – mixins and selector nesting among them – along with CSS Custom Properties replacing Sass variables.

Vanilla Extract, the main alternative we considered, by comparison largely doubles down on the tradeoffs of CSS Modules, but adds type safety and JavaScript logic to the toolkit. While these are very attractive attributes, unfortunately, it does not offer anywhere near as big a win as Tailwind CSS does on cognitive load, nor does it combat the large bundle sizes we have experienced with CSS Modules, where every new component grows our CSS output. Sprinkles, Vanilla Extract’s add-on library for generating utility classes, would generate unreasonably large CSS output if we used it to design our own style API that was as capable as Tailwind CSS, with no practical way to purge unused styles from our bundles. The maintainers of Vanilla Extract have confirmed to us that they do not intend to solve for this in the foreseeable future.

Feedback from Culture Amp engineers following our initial proof-of-concept experiments suggests that Tailwind CSS may well provide the intended boost to productivity and reduced bundle size in our application codebases, but that in our design system codebase, we may find that the need for custom CSS is significant enough to make Tailwind CSS a net liability. In particular, our current approach to simulating pseudo-states (hover, focus, etc.) for visual regression testing is difficult to replicate with Tailwind CSS’s utility class language, and components with complex logic for applying styles, or where we have a need to override basic component styles in a more specialised component, seemed not to scale well in our first attempts.

[Note: Since I wrote this in 2022, the Storybook Pseudo States add-on solved the challenge with pseudo-states, but our design system components’ CSS did indeed continue to require more complex logic than was comfortable to manage with Tailwind, so Kaizen has continued to use CSS Modules for now.]

If we try Tailwind CSS and find that we are still needing to write enough custom CSS that we miss having CSS Modules to keep it organised, then Vanilla Extract could be a good fallback option. It would let us use Sprinkles to cover 80% of our styling needs with a much less complete set of utility styles, and then use the framework’s first-class custom styling support to cover the remaining 20%. But it would be up to us to design, maintain and document that utility style framework, and it’s unlikely we’d catch up to the developer experience that Tailwind already provides.

Meanwhile, Tailwind continues steadily to eliminate use cases for custom CSS with each release.