My Thoughts on Tailwind

I am not very good at putting my thoughts into longer blog article form, so I am going to just write things like I would do on Twitter.

Note that all of these are just my opinions and feel free to disregard any and all of them.


I've been actually trying to use Tailwind in some projects recently and while I initially liked it, I've actually come to dislike using it.

I don't think it's a technically bad tool, but rather I believe that going all-in on utility classes actively makes your code worse. I've read Adam Wathan's rationale that inspired Tailwind and I actually agree with a lot of the stuff in it, but I disagree with the conclusions.

The Horizontal Scroll

My first illustration on what I think is a problem with Tailwind can be illustrated by the following button from the Flowbite component library:

text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 

Now, let's compare a basic conversion from Tailwind classes to CSS:

button {
    border: none;
    cursor: pointer;
    color: white;
    background-color: var(--tw-blue-700);
    font-weight: var(--tw-font-medium);
    border-radius: var(--tw-rounded-lg);
    padding: var(--tw-size-2) var(--tw-size-5);
}

button:hover {
    background-color: var(--tw-blue-800);
}

The biggest thing that turns me off the Tailwind version is that at a certain point, you have to scroll through a long horizontal list of classes. And I only converted a subset of the actual Flowbite button. The more complex your component gets, the longer you will have to scroll through a list of classes that you have to mentally translate to CSS properties.

In my opinion, the pure CSS version is a lot more readable because you can see everything vertically and you don't have to remember what every class does. I've also used custom properties (my favorite CSS feature) to more easily choose what values to use. In short, it's all of the advantages of Tailwind without any of the drawbacks.

Better Utility Classes

I'm not outright saying utility classes are bad, though. But you want components of the same type to look the same on your project, so I think it's a better approach to start by creating basic component classes (like 'button' or 'alert') and then use utility classes to create smaller variations.

There is more to it than just creating a one-to-one property to utility class conversion, though. If you have a bg-dark-blue utility class, I'd say it should also set the text color because when would you ever want to have a dark blue background without a lighter text color? And if you really need a different text color, you can add a separate class in the specific cases where you want it. But if you go the Tailwind route, you have to remember to add text-light whenever you use your bg-dark-blue.

The people who seem to treat Tailwind as their religion will say "you can easily do the same thing using @apply". They are technically right. However, not only does the Tailwind documentation explicitly discourage you, it also defeats the point of using Tailwind in the first place. You might as well just write CSS.

Don't forget that Tailwind adds an additional build tool to your development process. This is not much of a problem in itself, since we use a bunch of build tools all the time nowadays, but it's still something to keep in consideration. If you use pure CSS, you could just put your CSS file on any webserver and it will just work.

On Naming Things

Another big argument that people use to 'advertise' Tailwind is that you don't have to name things. This is the one argument that absolutely infuriates me in several ways. The first and biggest flaw with the argument is that you have to name things even when you use Tailwind. Unless you put your entire app in a single HTML file, you are going to have to extract a component that you have to name. If you have a Button component, you can add a button class.

If you genuinely don't know what to name something, do you even know what you're making? I understand naming things can be hard, but that's not a good reason to just throw away semantics altogether. And if you really don't know what to call something, just use a random word as a placeholder until you think of something.


update on 2023-06-18

Tailwind with Components

After playing around with Tailwind a bit more, I may have found a better way to use Tailwind than utility-only. Using either @apply or by writing plugins, we can create old-school component classes (like .btn). These components are placed in a components layer internally so we can still use all of the base Tailwind utility classes to override anything.

Taking a look at the various component libraries, there are actually quite a few of them that work exactly this way. In particular, DaisyUI, the self-proclaimed most popular Tailwind component library, uses this way of working and even makes the same points as me about the endless lists of utility classes.

Making a component plugin in Tailwind is fairly simple: you just use addComponents function with a class and the default properties you want to use. I have some example code below that shows how to create a simple button component with color variants.

Example component plugin
import plugin from "tailwindcss/plugin";
const DEFAULT_COLORS = [ 'blue', 'green', 'red' ];

/* inline-block rounded border border-indigo-600 bg-indigo-600 px-12 py-3 text-sm font-medium text-white hover:bg-transparent hover:text-indigo-600 focus:outline-none focus:ring active:text-indigo-500 */
const getButtonStyles = (colors, theme) => ({
    '.btn': {
        display: 'inline-block',
        borderRadius: theme('borderRadius.DEFAULT'),
        border: `1px solid var(--button-border, ${theme('colors.neutral.600')})`,
        backgroundColor: `var(--button-background, ${theme('colors.neutral.600')})`,
        padding: `${theme('spacing.3')} ${theme('spacing.12')}`,
        fontSize: theme('fontSize.sm'),
        fontWeight: theme('fontWeight.medium'),
        color: `var(--button-text, ${theme('colors.white')})`,

        '&:hover': {
            backgroundColor: 'transparent',
            color: `var(--button-text-hover, ${theme('colors.neutral.600')})`,
        },

        '&:active': {
            color: `var(--button-text-active, ${theme('colors.neutral.500')})`,
        },
    },

    ...colors.map(color => ({
        [`.btn-${color}`]: {
            '--button-border': theme(`colors.${color}.600`),
            '--button-background': theme(`colors.${color}.600`),
            '--button-text-hover': theme(`colors.${color}.600`),
            '--button-text-active': theme(`colors.${color}.500`),
        }
    }))
});

export const buttons = plugin.withOptions((options = { colors: DEFAULT_COLORS }) => {
    return ({ addComponents, theme }) => {
        const { colors } = options;
        addComponents(getButtonStyles(colors, theme));
    };
});

The same thing is also possible using @apply, although you lose the power of being able to use javascript. In my earlier writing, I was against using @apply because it went "against the spirit of Tailwind" and I didn't know for sure that it was always going to be around. My opinion on this has changed because I recently found confirmation on Twitter by the official developer that @apply will stay around. I'm also not a huge fan of framework developers telling users how they can use their tools, so everyone should feel free to use @apply as much as they want.

Tailwind for Prototyping

Everything else I've said aside, I do think the absolute best way to use Tailwind is for prototyping. Even if you rewrite all of your styles using vanilla CSS (or anything else), it is really fun and powerful to be able to quickly create a website just by adding classes. In long term projects I still think going utility-only is a very bad idea, but being able to quickly test things using Tailwind is very handy.