Don't use Tailwind for your design system, UI framework, or component system, whatever you prefer to call it. Tailwind isn't component-driven, although it claims to be, and you might struggle to develop a design system with it.
I have recently read a lot of opinions about Tailwind, including mxstbr's thoughts, jaredcwhite's opinion, and Tailwind versus BEM. I agree with many of their points, but I have a different perspective.
I'm focusing on the technical drawbacks of using Tailwind to create design-system components. I assume you're familiar with design systems and React and have basic programming skills. You can read more about design systems here or here.
A little bit about my experience
At the time of writing, I'm working at Draftbit. Our frontend is built on ReasonReact and uses Tailwind. It has about 500 components, and the main page, called the builder, looks like this:
screenshot-draftbit-builder
I have also contributed to a few design systems in my career and even written my own.
I'm obsessed with the conjunction of component-driven design and functional programming, and how they enable modular UIs.
First, the good parts
I think Tailwind is amazing, but not because of its utility-class patterns. Those patterns have existed for a long time. tachyons was created around 2016. Some people find utility classes pleasant to use, though I prefer to write CSS directly.
I think Tailwind shines for these reasons:
- Theme with strong defaults, beautiful and scalar: The config file generates the values needed for a scaled system based on design tokens such as spacing. For example, the defaultConfig generates a spacing scale from 0 to 96, or 0px to 24rem.
- Extendability: Extending values in the config file lets you represent any design and propagate the right CSS properties. Extending spacing, for example, generates values for margins, padding, width, min-height, and min-height.
- Just CSS: It isn't an abstraction or coupled to any framework. You can use it with minimal setup. All IDEs support it, all frontend and fullstack frameworks can integrate it easily, and there are tons of build tools to optimise performance. Because it's just "The Platform," it is easier to adopt and future-proof.
How is used in React
I use React as an example, but Tailwind works similarly with any other component-based UI library. React uses JSX through Babel or another transpiler, which transforms JSX into function calls and eventually DOM elements.
During rendering, React maps the className prop to the HTML class attribute, which applies CSS classes to the element.
This is the de facto way to use Tailwind with React. You will often see:
<div className="flex md:block w-32 h-full" />What's wrong with className
Editor-with-classNames
Hard to maintain
Removing one of the classNames from a list is error-prone. It resembles removing an unused CSS class from an append only stylesheet: classes can collide with and override other properties.
Linters and editor plugins can address that particular issue, but the problem remains.
Tailwind classes are also hard to change. Adding them can seem simple and fast when you create a component, but they slow you down when you modify or refactor it. Once your component has nested elements with many Tailwind utilities, you can't safely refactor its structure.
It is optimised for writing, not for reading
When I read JSX, I can comfortably imagine a one-to-one match with the UI. I can navigate the component tree and map it to the real-world interface.
screenshot-jsx-sketch-UI
You can build this UI in Tailwind, but somewhere inside the components you will find a layer packed with classNames. You first have to parse them in your head 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".
Fails at dynamic styling
Dynamic Styling means exposing a prop that changes a component's style. These props are often called variants. For example, a button component might have a variant prop that accepts primary, secondary, tertiary, and so on.
This dynamic styling pattern is common in design systems and hard to achieve with Tailwind. You need to reimplement the same Tailwind tokens as variables, map the variants to Tailwind classes, and then apply those classes to the component.
Creating a coherent, versatile, and scoped component API is already hard. It requires a good understanding of the problems the component is trying to solve. Letting Tailwind drive the styling values adds complexity that I find very annoying.
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 a simple example, suppose you want a component with a prop named gap. It accepts every spacing value that Tailwind accepts. You would write it like this:
const Card = (props) => {
const className = "p-" + props.gap.toString();
return <div className={className} />;
};
<Card gap={3} />;The Card component now accepts a padding value, which creates a few problems. gap could technically be any string, such as "4 flex", and break the entire UI. If you overuse classNames inside components, implementing the intermediate logic between your component API and Tailwind utility classes becomes a real pain.
Impossible to derive styles
Derived styles use JavaScript values to generate scalar UI. Because Tailwind uses utilities, it's tempting to reuse them in your components, which makes derived values impossible.
Consider a <Link /> component that starts with color="text-mono-100", where text-mono-100 represents the desired color. You may later need a different link color on hover. You could add hoverColor="text-mono-200" and call it a day. But representing color in another format is a nightmare because UIs often derive styles from props. In this example, you could represent the color in hexadecimal as color="#b54c4c" and derive hoverColor with 80% opacity. That would be possible if colors had a hexadecimal representation.
Tailwind's atomic language saves you from typing CSS, but it isn't made for component APIs that use a dynamic theme. It makes generative UI impossible, or at least very hard.
See this example: https://hihayk.github.io/shaper
Shaper-by-hayk
Breaks style encapsulation
I consider allowing className as a prop in a component API harmful. People often do this to enable external customisation of the 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 define boundaries, which is the original purpose of making a component.
Tailwind has no opinion on this. Still, when you need external customisation, it's tempting to accept any className from outside and add it to your classNames.
Stack: Example of variants
There are a lot of implementations of Stack. This is a screenshot of mine.
Stack places a list of elements on the Y axis, one above the other. It adds consistent spacing between them and moves them horizontally or vertically. It's a limited abstraction on top of flexbox. Designers define most of those constraints, and a component that enforces a fixed number of variants is generally a good thing.
Stack-documentation-taco
Composing at the wrong layer
The key feature of React is component composition. Here, composition means plugging components together like Lego to create more complex components from simpler ones.
In React, "components" are a set of rules applied on top of functions. Those simple rules let React provide many benefits that we take for granted. Dan Abramov explains them better in Writing resilent components.
As I mentioned before, appending strings to style a component feels like a step backwards. I prefer to compose components that each solve one problem.
Component composition gives React components these benefits:
- Declarative representation of the UI. Create complex pieces of UI from smaller ones.
- Decoupled: Isolate UI problems in black boxes that know nothing about their context.
- Variants: Implement variants of the same component without reimplementing different versions.
Example of component composition over Tailwind
This is a reimplementation of Charkra's UI Box component in a pseudo design system and Tailwind.
Card-from-CharkaUI
Here are 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 Tailwind recommends for extracting repeated utility patterns. Because it's a static definition, it only abstracts those lists into a CSS file. I don't want to get into much detail about it, but it doesn't solve the problems above.
When I would use Tailwind again then?
- Document-like websites, for styling content structured as one large chunk. Tailwind Typography provides good defaults for raw content such as a blog or newsletter.
- Prototyping, when the UI doesn't need to be visually polished or needs a unique style.
What should I use instead of Tailwind for my design system?
Not every team can invest time in building tooling and design systems that give the rest of the engineering team superpowers. Creating a design system is often a full-time job.
However, many people have spent a lot of time thinking about these problems and creating abstractions you can use:
If you still like what Tailwind offers, I recommend the approach we use at Draftbit. Create a thin layer on top of it. Treat all Tailwind tokens as code and keep Tailwind scoped within those components. Turn utility components that repeat in your code into stricter abstractions, and minimise Tailwind in your app.
How do I try to do it
I mentioned earlier that I made my own design system. It's a set of components concerned only with layout disposition. It has no opinions about cosmetics and lets you compose those components elegantly. It's called taco.
It's still a work in progress because many patterns remain unsolved, but I have used it for all my projects. Although it's public, it isn't for consumption. I didn't write all this as a plot twist to sell my library, but you can use it as inspiration.
Storybook and repository: https://github.com/davesnx/taco.
I hope this post helps. No tool is perfect, but understanding tools helps us use them wisely.