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:
- Lightning-fast build times since March 15, 2021
- Nested arbitrary selectors since June 7, 2022
- Multi-theme support since June 1, 2020
- Type safety (more on this below)
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.
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
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.