Blog of Raivo Laanemets

Stories about web development, consulting and personal computers.

Tailwind CSS experience

On 2023-10-26

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