Tailwind CSS experience
I have gathered some Tailwind experience over the last two years and decided to write about it. My learnings, both positive and negative, are from a couple of projects including an attempt to use it on the very same blog that you are reading.
Utility classes
Tailwind uses the utility-first approach that has been both
praised and criticized. It is a rather new framework
with the initial release in 2017. Utilities, such as CSS classes margin-small
, have been present in other CSS libraries. Tailwind
focuses only on utilities and makes them much less semantic. For example, small
is an unspecific size, instead it has class
m-2
which stands for 8px margin given the 4px base unit.
Components
Tailwind is meant to be used with component-based frontend libraries such as React, Vue, and Svelte. The components are a way to tie together the utility classes. For example, a React-based input field:
export function TextInput(props) {
return (
<input
type="text"
className="my-2 py-1 px-3 border-1 rounded-sm"
value={props.value}
/>
);
}
The className
prop with value my-2 py-1 px-3 border-1 rounded-sm
combines y-axis margin, padding, and border together with
the corner radius for the text input. You would reuse the same TextInput
component in every form of the application.
This approach makes custom CSS classes unnecessary. You would create a private NPM package to distribute these components if you need a consistent styling across multiple projects in your enterprise.
Variants
The component-based approach gives a nice way to support variations of the design. For example, an input with the error state:
export function TextInput(props) {
const className = clsx(
"my-2 py-1 px-3 border-1 rounded-sm",
"border-red": props.hasError
);
return (
<input
type="text"
className={className}
value={props.value}
/>
);
}
The component will have red border when the hasError
prop has been set. Other variations, for example, sizes, can be implemented
similarly. The clsx
function is from the clsx package to compose individual utility
classes.
Pre-generated HTML
Tailwind is unsuitable for pre-generated HTML from templates and themes. I tried to build a dark mode for my blog. It required a better approach to styling. My attempts to use Tailwind for the blog failed since my blog is generated from Markdown which emits standard HTML elements without a way to attach classes to them.
There are escape hatches to target normal HTML. In case everything else fails, you can still include arbitrary rules in the final generated stylesheet. Another option is to use a special syntax to target specific children elements:
<article class="[&>p]:my-4">
<p>This is the first paragraph ...</p>
</article>
I found it too difficult to read and edit.
Dark mode and theming
I learned that implementing dark mode is really inconvenient. Many components have to be aware of it. I wanted to apply it in a general way with CSS, like this:
body {
color: #333333;
background: #ffffff;
}
@media (prefers-color-scheme: dark) {
body {
color: #eeeeee;
background: #333333;
}
}
So when the user has dark mode enabled for their computer then the body text and background color would be swapped to light text on a dark background. Sadly such generic approach does not really fit Tailwind.
Tooling
Tailwind comes as an NPM package which provides the tailwind
command. The command is used to generate the initial configuration file,
index.css entry-point file, and later the final stylesheet file. The configuration file is for customizations, for example, to include your color
palette. The entry-point file imports base styles such as CSS reset but can also contain arbitrary rules like font definitions.
The tailwind
command will extract the used classes automatically. You will always get the minimal stylesheet. As a separate process it
will not interfere with your bundler, being it webpack, vite, or something else. The command can be easily run using the run-p
utility
from the npm-run-all package in parallel with your main bundler.
The VS Code plugin is pretty helpful. It can autocomplete utility classes in React components and automatically includes custom utility classes from the config file. Sadly, the plugin is unable to autocomplete in clsx expressions.
The element and style inspector in browsers' devtools works reasonably well with Tailwind classes. It's much cleaner and easier to
understand than random class names from some other solutions. Unfortunately, there is a huge number of --tw...
CSS variables dragged
around which makes the inspector usability not perfect.
Retrofitting
My first project to migrate to Tailwind was LightSketch. LightSketch is a custom light fixture design tool. It is not a huge application but has relatively complex code. It is written in Typescript and uses React.
The application started with Material UI. We had a designer to join our team but customizing Material was too difficult. Additionally, it caused bad performance due to deeply nested components. So we ripped it out and replaced with Tailwind. We are currently in the process of implementing the new design.
We migrated our components to OK-looking designs written with Tailwind utilities while waiting for specific parts of the design to be finalized. We are polishing the app piece by piece currently. I enjoy working with Tailwind, as do the other developers in our team.
Tailwind summary
Pros:
- Matches component-based libraries like React, Vue and Svelte
- Does not interfere with bundlers
- Simple to integrate with other tools
- Suitable for apps and websites with custom design
- Has reasonable IDE support
- Has reasonable debugging support
- No need to come up with semantic CSS class names
- Produces minimal stylesheet file
Cons:
- Does not fit pre-generated HTML
- Requires component-based approach
- Dark mode and themes are difficult to implement