It’s hard to follow a path if you can’t see it. I recommend putting your code guidelines in writing. They should be easy to follow and flexible enough to apply to many situations. Here are some of mine I’ve developed over the past decade:
-
Convention over cleverness
-
Verbosity over brevity
-
Ease of debugging
One project I’ve worked with violates these guidelines I hold dear. You may have heard of it: Tailwind.
Disclaimer
Nobody is perfect. I'm using Tailwind as an example here, but I want to make it clear that I'm impressed by and appreciative of the Tailwind team. Tailwind has brought innovative ideas that have improved build speeds and user load times.
Some devs love Tailwind and find that they are able to work faster with it. I don't. I'd like to talk about my rules using Tailwind as an example, but I want to make it clear that I'm not trying to besmirch the creators of Tailwind. They made a very cool thing.
Tailwind is faster to build with a smaller output than some other styling options, like CSS-in-JS. MUI, a popular CSS-in-JS design system, is currently building a new engine inspired by some of the things Tailwind does. By introducing new ideas, Tailwind is pushing web dev forward.
However, I do not agree with some of their syntax choices nor how debugging works with their system. They probably discussed these points and decided the trade-offs were worth it. Engineering is researching, planning, and then choosing the path with the best balance of pros & cons. I don’t like where they ended up, but I respect how they got there.
Convention Over Cleverness
Write your code with other people in mind. That other person might even be future-you.
I’ve seen people argue that Tailwind is just like CSS and that the knowledge transfers between them. I very much disagree. Tailwind gets clever with names instead of sticking with well-established CSS conventions. For example, what do you think this Tailwind class does? flex
As a CSS veteran, I had to look it up.
display: flex;
flex: 1;
Those both seem like possibilities. I figured it would be the second one, but turns out it’s display: flex
. Why generate confusion by introducing a shorthand when you could stick with the well-established CSS name?
Predictable code means sticking to conventions and naming patterns. Want to guess what the classes hidden
and invisible
do?
hidden >> display: none;
invisible >> visibility: hidden;
Maybe the W3C got it wrong in naming these properties, but it did so a long time ago, and there's no changing it now. Wouldn't display-none
have been far clearer? Plus, it's easier to find in the editor’s autocomplete.
That was a particularly heinous example, but there are others that made me question my sanity. Can you guess which one of the following Tailwind classes doesn’t exist?border-0 border-1 border-2 border-4 border-8
It's border-1
. Instead, use just border
to apply a 1px border. What?
You could maybe argue that the actual classes are all multiples of 2, so that makes sense somehow, but it's inconsistent, unpredictable, and frustrating. How many times have you needed an 8px border? Most of the time, its going to be 1 or 2 pixels.
I had to go verify that border-8 = 8px because my experience with Tailwind was mostly on spacing and margins are measured in units of 1/4rem (so mb-8 = 2rem bottom margin) which is its own maddening inconsistency. - Bradley Momberger, reviewer
Tailwind advocates might be saying, "Ok, but border
gives you a simple shorthand for the most common border width."
Ugh, kinda? This is only true for someone who regularly works with Tailwind. By having border
, Tailwind has established the higher importance of a 1px border but ignored the developer experience. I just had to go to the Tailwind docs to reduce the size of a border-2
because changing to border-1
didn't work. That is a massive waste of developer energy. This simple violation of Tailwind’s own naming convention broke my train of thought and forced me to research something that should have been mindlessly simple for someone who has been writing CSS since <marquee />
was a thing.
Verbosity Over Brevity
In the previous example of border
, Tailwind broke it’s own convention and it did it just to save 2 keystrokes.
Whether we are talking about JS variable names, CSS class names, or just code comments, character length does not matter to the final product. Your build system should be stripping out comments and minifying the rest. There is no good reason to hurt readability to save characters.
For classes that are a 1:1 with real CSS, which is most of Tailwind, I would have replaced the colon with a dash OR kept the dash and removed the spaces. Tailwind uses the dash for media queries, but there are some other possibilities.
/* Assume sm is a breakpoint size */
sm_display-flex
sm_display:flex
[sm]display:flex
Using a naming convention like this would be far more straightforward for people who are familiar with CSS, reducing the learning curve. The advantage of Tailwind is in build size, not naming. This "cleverness" of Tailwind hits me over and over again, sending me back to the docs for CSS styles I already know.
This naming convention would add maybe 25% to 50% to the space a list of Tailwind classes takes up. I don't think that matters much at all. Tailwind is already naturally verbose (as is CSS). Many style definitions already wrap to another line. Adding 50% wouldn't matter much. The build system is going to strip all these classes down to a few characters and the final build will be unaffected.
Ease of Debugging
HTML, JavaScript, or CSS, at some point, you'll run into unwanted behavior that you need to debug. Having tools to enable the debugging process is critical.
I should mention that Tailwind does have excellent editor plugins. The autocomplete works well. Not well enough to solve all the problems I mentioned earlier, though.
"Okay, then what's the debugging issue?"
When refining a layout, I open the site in the browser and make adjustments in the dev tools. This lets me see immediately what each CSS change does. With traditional CSS, you might have a singlelist-item
class that sets font size, padding, border, and color values.
<style>
.list-item {
font-size: 0.875rem;
line-height: 1.25;
padding: 0.5rem 0.75rem;
background-color: var(--background-light);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-md)
}
</style>
<li class="list-item">First thing</li>
With Tailwind, there are no unique classes. Instead, an element gets assigned multiple classes that each do a small thing, usually setting a single CSS property.
<li class="text-sm py-2 px-3 bg-background border-input border rounded-md">
First thing
</li>
By avoiding unique, component-specific classes, Tailwind can create a smaller stylesheet and therefore a slightly faster page load. However, this is a problem when debugging, as there is no simple way to adjust component styling using browser dev-tools. I can't simply bump up the padding on one group of elements using the browser tools because that same py-2
class can be used by several other elements on the page. Trying to adjust styling for one component type will also affect others. Adjusting styling in dev tools can be unpredictable with Tailwind.
One solution to the dev tools issue is to set the styling on that specific element as shown below, and that works for individual elements, but when you want to change a set of like components all at once, I’m not sure what to do. Tailwind is very fast, so I suppose I could make changes in the editor instead, but that’s still slower than doing it in the browser tools .
There was a time in the React world when Redux was extremely popular because the Redux dev tools were far superior to any other state management option. Redux is verbose with a steep learning curve and scales poorly, but the ability to debug effectively and efficiently was often worth it.
When you write code, do it in such a way that it's easy to debug and maintain. Some examples include:
-
Avoid choices that are not easily worked with in established debugging tools
-
Break code across multiple lines instead of fancy one-liners
-
Use descriptive naming to ease keeping track of what each variable holds
Stop picking on Tailwind!
Tailwind does get a lot of things right under the hood. Tailwind solves a lot of performance issues that come with CSS-in-JS, as the CSS-in-JS approach has to do a lot of work at runtime, while Tailwind is basically as performant as well-written CSS.
My major complaints center around the developer experience: naming and debugging. I find other options like plain CSS or CSS-in-JS to provide a better developer experience by far. CSS-in-JS is consistent in matching CSS, just moving from kebab-case to camelCase as it uses JavaScript objects. All I had to learn was that one simple kebab to camel naming rule, and all my CSS knowledge instantly transferred. Unlike with Tailwind, I have never needed to check the docs when styling with CSS-in-JS. I also did not need to adjust my debugging process.
The goal of this post isn’t to get you to stop using Tailwind. Instead, I want you to see how choices made while building Tailwind affect the developer experience. The choices you make while building your project will similarly affect future developers of your project as well. I can work around the debugging issue, so forking Tailwind to rename classes for consistency and CSS parity would result in a tool I would find useful.
What do you think about Tailwind?
Let us know on our Community Discord! We’d love to continue the debate conversation on the best CSS framework for frontend development.