What working with Tailwind CSS every day for 2 years looks like

For more than two years, I've been using Tailwind CSS almost every working day for company projects and a lot of weekends for my side projects.

During this time, I've worked with it on projects using WordPress, Laravel, Vue.js, Next.js, Remix.run, and many other technologies.

Working Smoothly across all of these technologies is one of Tailwind CSS's strong suits.

As is the case with every new piece of technology, it had its tradeoffs.

To spare you from listing the pros and cons of Tailwind CSS, I encourage you to check out the most comprehensive comparison I've seen from Shopify Polaris' team where Tailwind CSS got the most points.

Since this analysis have been conducted on Jun 18, 2021, Tailwind CSS and its community have produced a lot of what was declared missing in this analysis such as:

In this blog post, I'll be highlighting two of the cons mentioned in this analysis that I struggled with: type safety and the learning curve. I'll also be talking about my current workflow with Tailwind CSS.

Type safety

One of the main reasons I kept looking for a better alternative to Tailwind CSS was having no type safety. This brings back nightmares from Sass where you don't delete any of it cause you're not sure if it's being used or not.

Thankfully, we have Francois Massart, the creator of created eslint-plugin-tailwindcss, on our side.

Using its no-custom-classname rule, you can safely deprecate values or plugins in Tailwind CSS's config and know exactly where were they being used and deal with them appropriately.

Learning curve

With Tailwind CSS's v1 screencasts and great docs, it didn't take much time for me to get used to its conventions. However, I had a hard time translating the CSS I want e.g. height: 16px to Tailwind CSS's .h-4.

Both translating the CSS property and the CSS property value was challenging, but mostly the CSS property value part.

This required me to refer to the docs tons of times a day.

Translating CSS property values

My issue was mostly translating the CSS property value part that 4 in the spacing scale is equal to 16px.

Fortunately, Tailwind CSS made it very straightforward to configure values. Therefore, I was able to configure it so .h-4 or .h-4px would produce height: 4px. I'd also use .tracking-0.2px and so on.

Shortly after, I saw a comment from its creator regretting the default spacing values which confirmed I'm going in the right direction.

If I could do it over again I'd just use pixel values, like h-12 is .75rem (which is 12px by default).

— Adam Wathan (@adamwathan)

Nov 7, 2020

Translating CSS properties

While most of Tailwind CSS's choices for property values are easy to remember e.g. .h-* for height especially when you use them frequently, I kept struggling with CSS properties I use less frequently.

Especially where there is inconsistency in the naming convention e.g. if I wanted text-decoration-thickness: 2px;, I'd use .decoration-2. But if I wanted text-decoration-line: underline; it's .underline not .decoration-underline.

That prompted me to experiment with a more verbose version of Tailwind CSS using a custom plugin (and a lot of internal APIs).

This plugin would allow me to use my existing CSS knowledge by writing:

<div
  class="display-flex align-items-center gap-8px border-radius-6px padding-4px"
>
  <!-- ... -->
</div>

Instead of:

<div class="flex items-center gap-2 rounded-full p-1">
  <!-- ... -->
</div>

Sadly, this wasn't compatible with prettier-plugin-tailwindcss or eslint-plugin-tailwindcss. On top of that, it was using a lot of APIs from Tailwind CSS that I'd need to maintain.

P.S. If you're interested in this plugin, DM me on Twitter. I feel like there might be a place for a big enough of a company to make it compatible with community plugins and maintain it.

Coming to terms with the defaults

Both this approach or even modifying the default config values made it harder to use Tailwind UI and collaborate with teammates that are used to the default classes.

I accepted the tradeoffs and went back to using the default Tailwind CSS classes.

Also, I no longer bother to convert their values to pixels. Instead, I scroll down the suggested options from the Tailwind CSS IntelliSense VSCode extension since it displays the equivalent value for each in pixels once I write the peoperty part e.g. h-.

My current workflow

VS Code

Essential plugins I use are:

Formatting on save with the ESLint extension hangs a lot on my Mac M1 regardless of the workarounds I try, therefore, I fully rely on the prettier extension for formatting via the below config:

{
  "editor.formatOnSave": true,
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

For the few cases where ESLint highlights an error that's outside of Prettier's territory, I use VS Code's command pallet via cmd + shift + p and then select >ESLint: Fix all auto-fixable Problems.

Linting and formatting

I set up prettier through eslint to enforce formatting in the CI build step.

Here's where I enforce type-safety via no-custom-classname, auto sort classes via prettier-plugin-tailwindcss and avoid contridicting classes via no-contradicting-classname.

A basic .eslintrc.js config looks like the below:

/**
 * @type {import('@types/eslint').Linter.BaseConfig}
 */
module.exports = {
  ignorePatterns: ["**/*.d.ts"],
  extends: [
    "@remix-run/eslint-config",
    "@remix-run/eslint-config/node",
    "prettier",
  ],
  plugins: ["tailwindcss", "prettier"],
  rules: {
    // Enforce typesafety for Tailwind CSS classnames
    // https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/master/docs/rules/no-custom-classname.md
    "tailwindcss/no-custom-classname": "error",

    // Avoid contradicting Tailwind CSS classnames
    // https://github.com/francoismassart/eslint-plugin-tailwindcss/blob/master/docs/rules/no-contradicting-classname.md
    "tailwindcss/no-contradicting-classname": "error",

    // Format with prettier before eslint
    // https://github.com/prettier/prettier-eslint
    "prettier/prettier": "error",
  },
};

Abstractions

Implement the same visual patterns using the same code patterns, even if it's possible to optimize in some situations and use fewer HTML elements.

This makes things much easier to componentize and turn into a system.

<!-- 🚫 Don't -->
<!--  This card has two "sections" so the padding is on each section -->
<div class="rounded-lg bg-white shadow-lg divide-y">
    <div class="p-8">
        <p>Lorem ipsum ...</p>
    </div>
    <div class="p-8">
        <p>Lorem ipsum ...</p>
    </div>
</div>

<!-- This card only has one, so the padding is hoisted -->
<div class="rounded-lg p-8 bg-white shadow-lg">
    <p>Lorem ipsum ...</p>
</div>


<!-- ✅ Do -->
<!-- Separate the concerns so there are extractable patterns -->
<div class="rounded-lg bg-white shadow-lg">
    <div class="divide-y">
        <div class="p-8">
            <p>Lorem ipsum ..</p>
        </div>
        <div class="p-8">
            <p>Lorem ipsum ...</p>
        </div>
    </div>
</div>

<div class="rounded-lg bg-white shadow-lg">
    <div class="p-8">
        <p>Lorem ipsum ...</p>
    </div>
</div>

— Adam Wathan (@adamwathan)

May 31, 2021

Contcatanating classes

I have a simple utility function for concatenating classes that looks like this:

export function classnames(...classes: string[]) {
  return classes.filter(Boolean).join(" ");
}

Which i use it with ternaries like so:

<div
  className={classnames(
    "inline-block h-48 w-48 cursor-default select-none rounded",
    isDisabled ? "pointer-events-none bg-gray6" : ""
  )}
>
  {/* ...  */}
</div>

This approach has the advantage of keeping the support for Tailwind CSS' autocomplete and auto class sorting.

If that's getting out of hand, I'd recommend checking out Class Variance Authority. Although, it's not compatible with auto class sorting yet.

Custom classes

While it's easier to write custom classes in your .css file, I'd recommend doing so in the tailwind.config.js as it's compatible with the Tailwind CSS IntelliSense VS Code extension which gives you autocomplete and hover preview.

Conclusion

As of now with Tailwind CSS v3.2.4 and the ecosystem around it, I consider Tailwind CSS to be one of the boring established CSS solutions that enables me to be the most productive in building and maintaining projects of various sizes.