Don't use Tailwind for your Design System or your "UI Framework" or component system, call it whatever you prefer. What I meant is Tailwind isn't component-driven, which they claim to be, and you might struggle making a Design system with it.
Recently I read a lot of opinions about Tailwind (mxstbr's thoughts, jaredcwhite's opinion, Tailwind versus BEM) and nevertheless I agree with most of the points there I got a different perspective.
I will try to explain which are the drawbacks to make a Design system in Tailwind, focusing in the technical part of the components. Here I'm assuming that the reader is familiar with Design systems. Otherwise, can read more about in here or here.
A little bit about my experience
Currently I'm working at Draftbit, and our frontend is build on top of ReasonReact uses Tailwind, it's about ~500 components and the main page, called the builder
looks like this:
We have a design system on top of Tailwind (we might Open Source it at some point) and I contributed in a few Design Systems in my career, I even write my own.
I'm obsessed with the conjunction of design with functional programming and how those enable writing modular UIs.
But now, let's talk about Tailwind.
First, the good parts
The reason why I think Tailwind is amazing have nothing to do with their utility classes. Those utility classes have been around the Frontend community for a long time - tachyons has been created around 2016. Even some people found them pleasant to use I personally prefer to write directly CSS.
The reasons why I think Tailwind shines are:
- Theme with strong defaults, beautiful and scalar. The config generates all the values needed to produce a scaled system based on all those tokens. For example, spacing. The defaultConfig will generate spacing based on a scale from 0 to 96 that goes from 0px until 24rems. defaultConfig
- Extendability, being able to extend those values in the config brings the possibility to represent any design and being propagated to the right CSS properties. Ex, spacing would generate the values for margin, padding, width, min-height, min-height, etc.
- It's just CSS. It's not an abstraction, or coupled to any framework. ****Anyone can use it with minimal setup: all IDE would have support for it, all frontend framework can use it, there's ton of build tools to optimise their performance and long etcetera.
How is used in React
I use the example of React since I'm familiar with it, but I believe everything in this post applies to other frontend libs.
React uses JSX via Babel or any transpiler to transform JSX to function calls, and some props/attributes renames on the process, such as classNames
to class
(since it's a reserved keyword in JavaScript).
className
is used to apply CSS classes to React Elements, which at the end will be DOM Elements.
This is the defacto method to use Tailwind with React, you will often will see:
<div className="flex md:block w-32 h-full" />
What's wrong with className
Hard to maintain
It's error prone to remove one of the classNames
from the list, is a similar situation when you want to remove an unused CSS class in an append only stylesheet.
Aside from hard to remove, it's hard to change. Adding those classes might seem simple and fast while creating those components, but it will slow you down when modify or refactor them.
It is optimised for writing, but not for reading
When reading JSX, I feel confortable to imagine a 1-to-1 match with the UI. I can easily navigate thought the component tree and map with the reality.
Even that this snapshot of code-UI is doable in Tailwind, at some level of those components you will find a layer with a bunch of classNames
that you need to parse in your head in order to imagine the UI.
There's a famous quote floating around...
"Best code isn't optimised to be written, instead, it's optimised to be read".
Fail at dynamic styling
Dynamic Styling: Style your components with a global theme or based on runtime states.
Having a component API coherent, versatile and scoped is relatively hard by itself. Doing so, requires a good understanding of the problems that the component is trying to solve. Using Tailwind it's very tempting to use the same utility classes on that API.
There's a definite disconnect between a CSS API and a component API. For a design system, I care more about getting the component one right - @sarah_federman
For example, having a <Link />
component with color="text-mono-100"
. At the beginning it would make sense since text-mono-100
represents the desired color. Maybe later, appears a need to style the link with a different color on hover. You would add another prop called hoverColor="text-mono-200"
and call it a day.
The fact that color
is represented in another format it's a nightmare, often UIs derive styles from props. In the example above, you could have a color be their hexadecimal representation color="#b54c4c"
and derive the hoverColor
with a library.
The Tailwind language is nice to avoid typing CSS but isn't made for component APIs that use any sort of dynamic theme, making impossible (or very hard) to accomplish generative UI.
As an example https://hihayk.github.io/shaper
Breaks style encapsulation
I consider harmful allowing className
as a prop on the component's API. This is often made to have flexibility from the outside to enable any sort of customisation to your component.
const Button = ({ ...props, className }) => (
<button className={"flex text-mono-100 p-4 " + className} />
)
It's a trap, designing a closed API for those customisations would battle-test your component and force you to decide on an API that have some boundaries, which is the initial goal of making a component.
Tailwind doesn't have any opinion on this, but it's very tempting to allow any sort of className from the outside in your classNames
, given that you need customisation from the outside.
Stack: Example of variants
There're a lot of implementations of Stack. That's a screenshot of mine.
Stack places a list of elements on the Y axis, one on top of the other. Adds consistent spacing between and moves them horizontally or vertically. It's an abstraction on top of flexbox
, but limited. Those constraints are defined mostly by the designer, having a Component that enforces the number of variants it's generally a good think.
Composing at the wrong layer
The key feature of React is composition of components. Composition here means the possibility to plug those components like a lego which enables create more complex components based on more simple ones.
Composition is a concept that more or less you might feel familiar with it, which applies to many areas of Software development.
I see those "Components" are a set of rules that React forces on top of just functions. The rules are simple and allow the React library to perform many benefits that we take for granted. Those rules, are better explained by Dan Abramov in one of his posts, Writing resilent components.
As I mentioned before, appending strings to style your component feels like a step backwards. Composing components that are made to solve one thing, It's the pattern that I trend to prefer.
The composition of components allows React components, to benefit from
- Declarative representation of the UI. Create complex pieces of UI based on smaller ones.
- Decoupled: Isolate UI problems into black boxes that doesn't know anything about it's context.
- Variants Implement variants of the same component, without he need to re-implement different versions.
Example of component composition over Tailwind
Re-implementation of Charkra's UI Box component into a pseudo design-system and Tailwind.
Here you can see the different approaches to the same UI
<Box padding={5} width="320px" border="sm">
<Stack gap={2}>
<Image borderRadius="md" src="https://bit.ly/2k1H1t6" />
<Row gap={2}>
<Badge color="#702459">Plus</Badge>
<Spacer left={2}>
<Text
size="sm"
weight="bold"
color="#702459"
>
VERIFIED • CAPE TOWN
</Text>
</Spacer>
</Row>
<Text size="xl" weight="semibold">
Modern, Chic Penthouse with Mountain, City & Sea Views
</Text>
<Text>$119/night</Text>
<Row gap={1}>
<Icon src={MdStar} color="#ED8936" />
<Text size="sm">
<Text size="sm" weight="bold">4.84</Text> (190)
</Text>
</Row>
</Stack>
</Box>
<div className="p-5 w-32 rounded">
<div className="flex">
<img className="rounded w-full" src="https://bit.ly/2k1H1t6" />
<div className="flex flex-row mt-2">
<div className="rounded py-2 px-4 bg-mono-400">
<div className="text-mono-100">Plus</div>
</div>
<div className="text-sm font-bold text-pale-100">
VERIFIED • CAPE TOWN
</div>
</div>
<span className="text-xl font-semibold">
Modern, Chic Penthouse with Mountain, City & Sea Views
</span>
<span className="text-xl font-semibold">$119/night</span>
<div className="flex flex-row items-center">
<Icon src={MdStar} color="#ED8936" />
<span className="text-sm">
<span className="font-bold">4.84</span>
(190)
</span>
</div>
</div>
</div>
A mention to @apply
@apply
is the directive that Tailwind recommend to extract repeated utility patterns. I don't want to get into muchs details about it, but it does fail at the same problems as traditional CSS. Using apply it feel like just writting plain CSS, which in some of the cases it's amazing, but for a larger app it's a drawback.
When I would use Tailwind again then?
- Document-like websites, styling content that is structured as a big chunk. Using Tailwind Typography it does come with good defaults for raw content like a blog or a newsletter.
- Prototyping, creating a UI that visually doesn't need to be high quality or needs a unique style.
What should I use instead of Tailwind for my design system?
Not all the teams can have the possibility to invest time on building tooling and systems to give super-powers to the rest of the engineering team. In fact, create a Design system it's a full-time job.
But, there's a bunch of people who spend a lot of time thinking about those problems and tried to create a few abstractions that you could benefit from.
If you still like what Tailwind offers, I recommend a similar approach that we do at Draftbit. Create a tiny layer on top of it: Treat all the Tailwind tokens as code and maintain Tailwind scoped inside those components. Abstract those utility components that you found repeated in your code into a more strict version, and minimise Tailwind for your app.
How do I try to do it
Mentioned before that I made my own Design system, which is a set of components that only cares about layout disposition, doesn't contain any opinions about cosmetics and allows to compose those elegantly. It's called taco.
It's currently still a work in progress, since there's a lot of patterns that aren't solved yet. But I have been using them for all my projects.
Even that is public, isn't for consumption. I didn't write all of this for a plot-twist to sell my library, but you can use it as an example for inspiration.
Storybook and repository https://github.com/davesnx/taco.
I hope this post doesn't get in the wrong form, any tool is perfect and using those to solve problems is part of who we are.
Top comments (2)
I absolutely agree with your points.
While building our design system on top of tailwind we also faced similar issues and the most annoying issue is the specificity issue.
Now it's not the issue with tailwind per say but using atomic CSS for any component library will be a headache, and you have to juggle with specificity knives.
Here's a summary tweet of problems we faced with tailwind class specificity
I didn't have the issue with specificity since we wrote a bunch of logic that abstract this away from having similar utilities on the same property, but I see where this error may occur.