Tailwind CSS Pain Points and Solutions
Cannot Access DOM Nodes
The beauty of Tailwind is that you can write CSS directly on the DOM, without frequently switching context, without worrying about selectors, and almost WYSIWYG editing styles on DOM nodes.
But sometimes we cannot access the DOM nodes. How should we style those nodes then?
What does it mean by not being able to access nodes? It could be that you introduced a third-party component that contains dozens of internal DOM nodes, but for you they are all encapsulated. In this case, how can you use Tailwind to customize the style of an internal node?
Here are a few solutions:
1. Use Traditional CSS Files
First, Tailwind CSS does not conflict with regular CSS. You can still create a .css file to handle scenarios that are difficult with Tailwind. Of course, this is the least recommended method and should only be used when it’s really tricky. Generally, once you decide to use Tailwind, there should be no .css files in the project besides the global reset.
2. Choose a Tailwind-Friendly Component Library
If third-party components make you feel lost, consider choosing a component library that is friendly to Tailwind adaptation. For example, shadcn generates source code in your project source, so it’s easy to customize since the source is at hand. Another is the well-known MUI, which supports multiple style customization approaches. Simply put, such libraries break each component into basic building blocks and allow users to perform slot-like DOM replacement and className renaming on those blocks.
3. Use Tailwind Selectors
Tailwind provides some selectors to increase flexibility. See this link. Of course, large-scale use of such selectors is not a good practice; it’s a bit like black magic. Let’s take the shadcn table component as an example. To unify the style of every DOM under the table header, it uses the descendant selector [&_tr]:.
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
<thead
data-slot="table-header"
className={cn('[&_tr]:border-b bg-primary', className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
<tbody
data-slot="table-body"
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
)
}
Poor Readability
Tailwind writes styles into the DOM, which on one hand improves style readability, but on the other hand, if you only want to view the DOM structure, the long strings of className can hinder readability. How can we improve readability?
1. The cn Function
i.e., classnames, my most recommended solution. Essentially it’s the same as Vue’s style binding. When the logic is true, the className takes effect. We can establish conventions based on it to organize className by type. For example, layout-related classes like flex justify-center align-center are grouped together; box model classes like width and height are grouped; font-related classes are grouped. In short, classifying long strings in a regular pattern can improve readability to some extent.
import cn from 'classnames'
const MyComponent = () => {
return (
<div
className={cn(
'flex justify-center align-center',
'font-mono font-light text-blue-500',
'py-3 px-2 mt-3'
)}
/>
)
}
2. UnoCSS
It’s certainly annoying to call a function and pass multiple strings every time. That’s why I recommend UnoCSS, which improves the experience of writing Tailwind CSS.
3. Use Editor Plugins
Use inline fold or tailwind fold plugins in VS Code. By default, these plugins collapse the className, so you can’t see them and won’t be annoyed. But I personally think this plugin is more suitable for reviewing others’ code. If you write a lot of code every day, the plugin can be a bit obstructive.
4. Use @apply Wisely
If you are the only frontend developer on this project, you can use @apply to encapsulate some classes. For example, define flex-center or base-button. But try to use it sparingly; if the project is full of @apply directives, you might as well go back to regular CSS or SCSS.
Difficult Debugging
Compared to debugging atomic CSS, we prefer debugging traditional CSS because traditional CSS groups related styles together, making the definitions clear at a glance. Is there a way to give Tailwind CSS a similar debugging experience? Yes.
Use a Browser Extension
This browser extension is made for this problem: https://github.com/astahmer/atomic-css-devtools
It adds a new tab where related definitions are clear at a glance.
Use Semantic ClassNames
Another suggestion: not every className needs to be a Tailwind utility. You can still write some semantic classNames. Even though they don't have corresponding styles, they can help you quickly locate DOM nodes and improve debugging efficiency.
Styles Not Working
Another common issue. CSS style application involves some complex specificity. You can read this article. Atomic CSS simplifies specificity to some extent. You can configure Tailwind so that all your utilities have the highest specificity, for example by prefixing them all with the root element. However, be careful that components using portals might lose styles because they are not children of the root element by default.
Dynamic className Issue
Another important point: Tailwind CSS compilation works by scanning the project source files with regular expressions (or other pattern matching methods) to find possible classNames, and then compiles them into actual CSS code. So a className like the following will not work:
<div className={`bg-red-[${isDark ? 600 : 200}]`} />The solution is to write it like this:
<div className={isDark ? 'bg-red-600' : 'bg-red-200'} />Both of these are classNames that can be scanned and correctly compiled.
Style Conflict Issue
Another case where styles don't work is possible className conflicts. The following code is very common, allowing our components to support style customization to some extent:
<div className={'some class name text-red' + externalClassName} />Let's assume that if text-blue is passed, what color should the font be, blue or red? Actually, it's uncertain. Unlike inline styles where later styles override earlier ones, these are all compiled into a CSS file. The priority depends on the build tool's processing logic and Tailwind's CSS generation order.
If you have used shadcn, you know the solution:
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}The core is the call to the twMerge library. It ensures that the later styles win when there is a conflict. If your own component library uses Tailwind, this cn function should be widely used.