5 best practices for preventing chaos in Tailwind CSS
Translations
Working with Tailwind CSS is pretty fast and easy (that’s why it’s received such wide recognition). You just paste a list of different classes in your HTML—and your interface immediately becomes attractive! But, as the application grows, the lists of classes grow. Then, one day you realize you can’t understand your code, you’re confused with the structure of the application and magic variables, and work becomes a struggle. This article is all about avoiding this scenario, sharing some best practices to ensure you stay aloft when using Tailwind CSS.
We can prevent any headaches and resolve any problems (for the most part) by using Tailwind accurately and wisely. But, there are two requirements your project must met, and if it does not, Tailwind can make your job very difficult instead.
First, you should have a design system in your project. Tailwind’s philosophy goes alongside the design system where designers and developers use consistent design tokens. Design tokens are atomic values (like colors, spacing, or typography scales) that define a design’s properties and that are reused throughout the project.
Let’s imagine that we have a standard button and some tabs that need to be the same color as that button:
.button {
background-color: oklch(45% 0.2 270);
}
.tab {
background-color: oklch(45% 0.2 270);
}
If we decide to change the color scheme of the project a little, we’ll need to find every instance of this color (which looks like a magic variable) and update them everywhere. This can be inconsistent and harder to maintain.
Irina Nazarova CEO at Evil Martians
Design tokens help prevent these problems and ensure uniformity across UI elements.
Luckily, to implement design tokens, we only need to to define the tokens in tailwind.config.js
:
module.exports = {
theme: {
colors: {
primary: 'oklch(45% 0.2 270)'
}
}
}
After adding a new color with the name primary
, we can use bg-primary
for our background color or text-primary
for the text color throughout the application:
<button class="bg-primary">Standard button</button>
<div class="bg-primary">First tab</div>
This way, when you want to change the color scheme in the project, you only need to replace the color in one place: tailwind.config.js
.
It’s better to avoid using Tailwind if you haven’t considered a design system because you’ll have to write magic values in the class lists (like 'p-[123px] mb-[11px] gap-[3px]'
) or add a lot of new tokens (15px
, 16px
, 17px
in the spacing config), and this will eventually bring a lot of mess to your code.
Having a consistent design system is good because it can help the development and design teams understand each other better.
For instance, within Figma, you can have a single shared source of truth for any values in your design system. But to make this system truly maintainable, you’ll need to introduce some conventions regarding token grouping and naming—which we’ll get into later in this article.
This is the second requirement your project needs to meet: you should already be using a component-based approach. The utility-first approach can lead to quite cluttered and verbose HTML structures since Tailwind classes apply directly to elements. This can mean the markup is harder to read and maintain, especially noticeable as your project grows.
The solution: actively using a component-based approach that encapsulates frequently used patterns (in our case, HTML elements appearing more than once) as separate components.
With this approach, we can keep things DRY. Moreover, we’ll still have a single source of truth for our Tailwind styles, and we can easily update it together in one place:
<!-- Reusable button with a long list of Tailwind classes: -->
<button class="bg-yellow-700 border-2 font-semibold border border-gray-300 text-green p-4 rounded">
Custom Button
</button>
<!-- Instead of repeating this structure over and over again, create a reusable component: -->
<CustomButton>Custom Button</CustomButton>
If your development tool doesn’t allow you to split your code into components, it’s likely that the utility-first approach of Tailwind will only make development harder, and you should probably look to other CSS frameworks-for example, CSS Modules.
And one last thing regarding a component-based approach: avoid using the @apply
directive:
.block {
@apply bg-red-500 text-white p-4 rounded-lg active:bg-blue-700 active:text-yellow-300 hover:bg-blue-500 hover:text-yellow-300;
}
Yes, using this directive, your code may look cleaner, but it throws away the key advantages of Tailwind: less mental overload when coming up with names for CSS classes, and the absence of regressions when changing styles (since with @apply
they won’t be isolated within the component). Further, using it increases CSS bundle size.
The creators of Tailwind have also highlighted the importance of using the @apply
directive with caution in the documentation.
If you met both requirements, Tailwind CSS is likely a good framework option for you! Here are the most helpful practices for improving your long-term experience with it.
1. Use fewer utility classes when possible
When you build a list of utility classes for an HTML element, each new class adds additional complexity for the developers, and they’ll have to analyze and work with the code later (and this includes you, too). Of course, these lists are an essential and inherent feature of Tailwind, but nevertheless, it’s better to write as little utility classes as possible.
Here are a few ways you can decrease the number of classes and get exactly the same results:
- Instead of setting
pt-4
pb-4
, you can just usepy-4
. This also applies with thepx
,mx
, andmy
properties. - Instead of
flex flex-row justify-between
, you can just useflex justify-between
. This is becauseflex-row
is the default value of theflex-direction
property in CSS. In general, it can be valuable to remember some default values of other CSS properties (flex-wrap
, for example) to make it easier to spot use cases like this. - Instead of writing a long class list like
border border-dotted border-2 border-black border-opacity-50
, you can setborder-dotted border-2 border-black/50
and this will have the same effect:border-2
implies thatborder
is set, andborder-black/50
represents a shorthand for the RGBA format.
With a shorter list of classes, the next time you inspect the structure of your application, it’ll be much easier to analyze what’s going on.
2. Group design tokens and name them semantically
When working on a team, you probably agree that some clean coding practices (like the clear naming of variables) are really important for long-term development. That said, even if you’re working alone, it also can be worth setting some rules for code clarity, otherwise, you could get confused about your own project (for example, when returning after a break).
This approach is especially important while working with Tailwind because reckless usage of such a large number of classes and design tokens can really bring confusion into your code.
As discussed above, using design tokens is a great practice, but just pasting them haphazardly can lead to chaos in your tailwind.config.js
file.
To remedy this, group related tokens together in tailwind.config.js
. This means that design tokens for breakpoints, colors, and so on, will be in specific areas and won’t mess with each other:
module.exports = {
theme: {
colors: {
primary: 'oklch(75% 0.18 154)',
secondary: 'oklch(40% 0.23 283)',
error: 'oklch(54% 0.22 29)'
},
spacing: {
'sm': '4px',
'md': '8px',
'lg': '12px'
},
screens: {
'sm': '640px',
'md': '768px'
},
},
//...
}
Here’s another important thing: keeping a single semantic naming convention for your tokens will make it easier to find the necessary tokens and expand the system as the application grows.
For example, to add a color for your error state, don’t just copy and paste the bright-red
token from your Figma file into your Tailwind configuration: put it into the colors section and give a more concise name like error
. This will make the system much more consistent.
3. Keep class ordering
Here’s another clean coding convention: using a consistent order makes classes easier to read and understand. To illustrate, let’s take a look at some HTML elements with unsorted classes:
<div class="p-2 w-1/2 flex bg-black h-2 font-bold">
First block with unsorted classes
</div>
<div class="italic font-mono bg-white p-4 h-2 w-3 flex">
Second block with unsorted classes
</div>
In the blocks above, there are classes for different categories: dealing with the box model, display, typography, and so on—but they don’t have any sort of presentational order. We can apply a unified order to sort classes by categories:
<div class="flex h-2 w-1/2 bg-black p-2 font-bold">
First block with sorted classes
</div>
<div class="flex h-2 w-3 bg-white p-4 font-mono italic">
Second block with sorted classes
</div>
Since maintaining class ordering manually requires a lot of time and attention, it’s much better to automate this work using the official Prettier plugin for Tailwind CSS. To learn more about how to get started with it and the methodology of how the classes are sorted, we recommend reading this article.
4. Minimize build size
It’s important to keep bundle size as small as possible—heavy builds mean slow-loading pages, bad performance, and frustrated users.
Tailwind provides us with thousands of utility classes, and it’s unlikely we’ll use all of them within a single project. So, how can we make sure that any unused styles won’t end up in our production build?
If you use Tailwind version 3.0 or above, the Just-in-Time (JIT) engine is enabled in your project by default—it ensures that CSS styles are generated as they are needed, and we won’t need to purge unused styles for production builds.
But if you’re using an older version of Tailwind, you need to perform additional optimizations for your build—this can be done using PurgeCSS, a tool for removing unused CSS. This article explains how to do this in version 2.1 and older. You can also enable the JIT mode manually in your tailwind.config.js
file, like so:
module.exports = {
mode: 'jit',
...
}
This will make sure that we’re only including the necessary styles in our bundle.
There is another important thing to consider: always minify the final CSS for the production build. Minification removes all unnecessary characters (like whitespace, comments, and so on) and this will noticeably reduce file size.
Using the Tailwind CLI, this can be done by setting --minify
flag:
npx tailwindcss -o build.css --minify
Or, if you’ve installed Tailwind as a PostCSS plugin, you can use the cssnano tool for minification by adding it to your plugin list.
If we don’t consider optimization, the size of our CSS can end up really big (more than several tens of kilobytes). Even in a small project with a few components with styles, there is a 30%+ size difference after minifying CSS and enabling JIT mode. To achieve this, you just need to add the minify
flag and enable jit
mode, as described above.
If you want to learn more information about minification and compression for Tailwind, check this section of documentation.
Tip: If you have design tokens in your project, make sure that they’re all actually being used. Unused design tokens confuse developers, make the configuration more complicated, and introduce unneeded messiness into your design system.
5. Prevent inconsistencies when overriding and extending styles
Imagine that we use a component with a custom button on our page:
<Button className="bg-black" />
And we have a Button
component that has some default style:
export const Button = () => {
return <button className="bg-white">Test button</button>
}
In this case, the button will remain white–Tailwind doesn’t automatically override style and apply the black color, so we need to specify it in the Button
component:
export const Button = ({ className = "bg-white" }) => {
return <button className={className}>Test button</button>
}
There’s nothing inherently wrong about this aspect of Tailwind, but if we want to customize some appearance by overriding or extending a lot of styles, it can be cumbersome to pass classes via props each time.
Moreover, there is one more drawback to this approach: accepting utilities via props can make it harder to ensure a consistent component view. This approach encourages using any utility combination for the same component across the app which can lead to a lack of visual consistency.
So, what can we do with it?
Instead of allowing any arbitrary utility classes to be passed via props, define a set of predefined variants:
const BUTTON_VARIANTS = {
primary: "bg-blue-500 hover:bg-blue-600 text-white",
secondary: "bg-gray-500 hover:bg-gray-600 text-white",
danger: "bg-red-500 hover:bg-red-600 text-white"
};
Then, change the Button
component so it can accept a variant
prop. To make constructing className
more convenient, you can use clsx:
export const Button = ({ className, variant = BUTTON_VARIANTS.primary }) => {
return <button className={clsx(className, variant)}>Test Button</button>
}
Tip: using clsx
would be also especially handy if you need to construct classes conditionally.
After constructing className
for the component, just use it, passing the desired variant:
<Button variant="secondary" />
Now, consistency is ensured, and despite the fact that we added a restriction on full customization, flexibility remains; we can add any new variant for the component or edit an existing one.
And the other benefit of this approach is that it allows for simpler maintenance: changes to utility classes can be made in one place, and then propagated to every component of that variant in the app.
If for some reason you don’t want to use the sets of predefined variants, you can try the package tailwind-merge, which provides utility function twMerge
to merge Tailwind classes in JS without style conflicts–but it should be used carefully and only when necessary, since it is not the most lightweight and increases bundle size.
Summing up: how to use and how not to use Tailwind
Tailwind is a powerful tool, but it’s important to use it while following some rules to prevent chaos from erupting in your project. Let’s sum up the principles that we listed above.
First of all, to get the most out of these practice, you should use Tailwind when you already have a design system and consistent design tokens and have opted for a component-based approach. Without breaking reusable elements into components, using Tailwind will become painful sooner or later, leading to repetitive or verbose HTML structures.
- Minimize the number of utility classes where possible
- Formulate code conventions within your team, for example, by grouping design tokens and naming them semantically
- Likewise, implement consistent class ordering and set up linters to ensure code cleanliness
- Minimize your bundle sizes: ensure you’ve included only the needed styles, and always minify the final CSS for production build
- When appropriate, try to define a set of predefined variants for your components; this will help avoid problems with inconsistencies and style overriding
By following these rules, you’ll be able to use Tailwind for the long haul–with pleasure and without problems–giving your team the chance to revel in all the benefits it provides.
At Evil Martians, we transform growth-stage startups into unicorns, build developer tools, and create open source products. If you’re ready to engage warp drive, give us a shout!