Don't compare TailwindCSS with CSS

A year ago I was teaching web development students about the various approaches to writing and organizing CSS, which is notoriously hard to maintain, in its “natural” form. Among the things I was showing was a brief mention of the “atomic CSS”-approach and the fact that TailwindCSS had gained a considerable amount of steam over the last few years. Back then, I was playing it down as a fad, telling them I didn’t understand what all the fuss was about, basically telling them it was unjustified hype.

Fast forward to this semester, when I taught the same course to another class. My view had changed. I told my students, that I now believe there are good chances TailwindCSS will dominate the space for years to come, similar to how Bootstrap had been doing in the mid-2000s.

Plain CSS is not Tailwind’s competition

So what changed? During the months in-between I had realized, that the issues I had with Tailwind were invalid. I was comparing it to plain CSS, even though I had not been writing plain CSS - or even SCSS, Less, etc. - outside of my teaching engagements for quite literally years.

What Tailwind is actually up against

Joining a ReactJS-legacy project you will quite likely find a stack similar to this:

  1. TypeScript
  2. NextJS
  3. React (duh)
  4. Artifacts of a past Redux integration (“we tried to delete it, but when we do it breaks and we don’t know why”)
  5. styled-components or emotionJS
  6. styled-system

Let’s focus on the last two points. These have likely been integrated because keeping extra CSS files for every React component quickly becomes unmaintainable with a growing project. So a decision was made to keep the styles within the component files. Eventually, it turned out to be cumbersome to write a stylesheet for every component in your application. To the rescue comes styled-system, which provides you with reusable components using inline styles via props, comprehensive theming functionality and an intriguingly simple way to write responsive styles.


None of this comes without a cost. The whole convoluted sentence you just decided not to read, is computed at runtime and believe me when I tell you, a peek under styled-system’s hood reveals some treacherous caveats.

So what if we could have all of that power, but none of that runtime overhead? Sounds too good to be true? Well, let’s take a look at how we would replace styled-system code with TailwindCSS in this simple example.

const SomeComponent: FunctionComponent = ({children}) => (
    px='md' // assuming a theme exsists where this is defined
    py='sm' // assuming a theme exsists where this is defined

These are six props that need to be computed at runtime by the browser’s JavaScript interpreter. If we would use Tailwind, this code would be to the same effect:

const SomeComponent: FunctionComponent = ({children}) => (

As we can see, the ideas at play are the same. In both examples, we

  • use values defined in our theme/config for the padding
  • use arbitrary values for the positioning and width
  • defined all of the styles inline


Let’s take a look at responsive styling. I will just show single lines for brevity. This is how you might change a components margin depending on screen size in styled-system.

  m={{sm: '4px', md: '8px', lg: '16px'}}

Easy enough, but note that added to everything else discussed above, the passed-in object gets created every time this component re-renders. (Strictly speaking, this is a constant value, that should be moved out of the component entirely, defeating the whole point of readable responsive code.)

On the other hand, doing the same thing in Tailwind looks very similar, but doesn’t require unnecessary computing when running in the browser:

className = 'sm:m-1 md:m-2 lg:m-4'

It goes even deeper…

How would you give a component a hover effect in styled-system or styled-components? Inline CSS, right?

    &:hover {
      background-color: red;

Now if that isn’t some ugly code. Let’s compare this to what tailwind has to offer:

<div className="hover:bg-red-500">

The “but tailwind is hard to read” argument backfires at this point. Tailwind offers the same principles with most of the other pseudo-selectors and media queries you would normally use: focus, active, visited, first, last and even even. If you are using animations or transitions, there is even a modifier to handle the prefers-reduced-motion media query with motion-reduce.

… but it won’t do that

Inexplicably, Tailwind does not yet support the pointer media query, which I frequently used to enlargen clickable areas for buttons on touch displays. There seems to be a plugin in development, but its activity doesn’t stoke confidence.

Aside from this minor nuisance, there are also natural limits to the atomic CSS approach. The one that had the most impact on me (while coding this very blog, actually) is the fact that there is no way to style an element while another element is interacted with. Consider the following SCSS, in which an image gets shown when its sibling text is hovered over.

.image {
  opacity: 0;

.text:hover {
  & ~ .image {
    opacity: 1;

It is not possible to emulate this behavior with atomic classes alone, in fact, whenever we want to express relationships between certain elements, we have to deviate from the atomic CSS principle, by writing custom classes in our entry CSS file and applying those to our elements.

Edit 16.05.2022: This might not hold true for much longer, as there is an upcoming feature called “arbitrary variants”, which solves this exact issue. I will refactor my custom classes after the feature’s release and share the process on this blog.


Comparing TailwindCSS to plain CSS or even Sass/SCSS, you might question its benefit. When did you last write plain CSS though? Modern projects often rely on complex, component-based architectures, for which appropriate styling methods have evolved, and have long reigned supreme in the space. When compared to these styling methods, it becomes clear why TailwindCSS has gained such a strong following over the past years.