Don't use Tailwind for a design system (2021)
sancho.dev
sancho.dev
const headerClasses = [(list of Tailwind classes here)];
<header className={...headerClasses}>...</header>
because the complexity of reading and writing all of the classes is just too much. At that point, you've just reinvented CSS classes. Tailwind fans will tell you to not do this but if multiple companies are independently having the same problem and coming up with the same solution, the onus is not on the user anymore, it's on the creator to fix it. @apply can work but again it's really not recommended by Tailwind itself, for whatever reason.These days I recommend learning CSS really well and then using Vanilla Extract (https://vanilla-extract.style), a CSS in TypeScript library that compiles down to raw CSS, basically using TS as your preprocessor instead of SCSS. For dynamic styles, they have an optional small runtime.
They have a Stitches-like API called Recipes that's phenomenal as well, especially for design systems, you can define your variants and what CSS needs to be applied for each one, so you can map your design components 1-to-1 with your code:
import { recipe } from '@vanilla-extract/recipes';
export const button = recipe({ base: { borderRadius: 6 },
variants: {
color: {
neutral: { background: 'whitesmoke' },
brand: { background: 'blueviolet' },
accent: { background: 'slateblue' }
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
},
rounded: {
true: { borderRadius: 999 }
}
},
// Applied when multiple variants are set at once
compoundVariants: [
{
variants: {
color: 'neutral',
size: 'large'
},
style: {
background: 'ghostwhite'
}
}
],
defaultVariants: {
color: 'accent',
size: 'medium'
}
});Impressive that OP still is using ReasonReact, I thought it was all but dead after ReScript.
I still love using it, and will continue to do so. This isn't enough to stop me from using it. Normally I try to avoid this, and abstract the repeated bundle of tailwind classes in a component instead (it can be a container component or just some useful utility component to avoid having to repeat the same group of classes too much). This can have its own drawbacks, because having too many custom components that you need to remember to use in given situations (which is effectively how most design-systems would work anyway) makes the codebase less approachable.
And even still, we'll end up using the above trick (referencing a group of classes bundled into a property of an object somewhere else) from time to time, as well as the obvious option of just repeating classes as necessary. Referencing classes from an imported object is annoying too, because it breaks intellisense.
I'm honestly not sure what a better solution is though, because Tailwind has really sped things up for me compared to CSS/Sass/Chakra/Material.
No worry about how you reference things or static-ness etc and the final output can be flattened down to just div + css even across module boundaries or when nesting multiple times.
The fact is @apply is there and if we're using it and there's enough push-back, I don't see why the Tailwind team wouldn't listen to that or create a workable upgrade path for those who wish to continue to use it.
So use the tools you have - don't avoid it just because the core team dislike it. They can't remove it in the version you have installed today (unless they have some magical way to reach into your computer/server)
I keep being surprised that noone bring this up in these discussions.
I think this is the main reason why Tailwind took off so fast. The frontend tooling world forgot the majority of its users.
BUT, then what if you want to add some kind of special behavior to your button that involves subcomponents, e.g. a loading state that conditionally renders a spinner inside the button? Or you want to provide convenience props like rendering an icon before or after the button text? Then a class is not enough.
If you're building a design system/component library, none of these options are simple and there are always tradeoffs.
Perl, maybe, but not APL. CSS version of APL would let me style my personal site and blog in less characters than it took me to write this comment :).
> const headerClasses = [(list of Tailwind classes here)];
> <header className={...headerClasses}>...</header>
Why wouldn't they extract the header as a higher-order component? That would make more sense even without Tailwind, and I haven't heard anyone in the Tailwind community advocate against that.not true, separating components by concern can save you a lot of fiddling with memoization
It's not necessary but in the given case it's obviously useful as there is more to abstract than just the html element - the styling.
Sort of, except instead of context switching between CSS, HTML, JS and your programming language, you can now remove the CSS entirely. Less context switching is good.
I'm a Tailwind fan and I don't see a problem with that pattern for some cases. Obviously there are better ways to organize that should be preferred in general, but it's fine to do that here and there.
<button
bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
>
https://windicss.org/features/attributify.htmlBut I think the author's main gripe was that component abstraction was made difficult with Tailwind when you want to customize the component from the outside, by dynamically altering the styles through props (as often needed for contextualization in design systems), and not just no-prop components like Tailwind suggests: https://tailwindcss.com/#component-driven
None of these "negatives" are "negatives" for using it as part of a standard HTML/CSS website outside of React. The comparison between readability of a bunch of divs and web components or react makes no sense, of course <SidebarItem /> is easier to read then <div class="flex-1 bg-blue font-bold, text-md d:none"></div> but nobody writes React like that. Everyone will have a SidebarItem component that spits out that div.
I don't know the purpose of misleading title, I guess this is how web works now - capture attention by misleading readers, then criticize the actual layer that causes problems, but hide the fact that the one in chair is at fault for not being a capable user of technology.
<SideBarItem />
as export { SideBarItem = (<div class="flex-1 bg-blue font-bold, text-md d:none"></div>) }
Is that a bad thing? (Genuinely asking for opinions)E: added clarity
It's how (nearly) everyone using React (or any frontend framework) + Tailwind will structure their code. And I'm not sure the author is arguing against Tailwind's utility in static styling scenarios.
I think the article's author would argue that once you move beyond static classes that Tailwind's class building becomes messy.
So <SideBarItem padding={4} active={true} /> would be cleaner in the authors mind if the exposed props get applied by some other tooling better suited for dynamic styling instead of simple string manipulation.
There is some merit to that argument. Building the class string can be cumbersome in some scenarios. But Tailwind "clicks" for me where other solutions do not. So I do it anyways.
Or if it's a bad thing, I'm guilty too
YMMV; I prefer styled/ emotion-styled for primary, reusable blocks, and tailwind for one-off exceptions like a bit of extra margin.
<div class="side-bar-item"></div>
If it's "possible" people WILL write code like that. That's why I like styled-components, you are FORCED to separate the style definition and then you can say nobody writes code like that. But dozens of style classes mixed with functionality? People DO write code like that, a lot more than I like admitting seeing myself.
CSS brought some opportunity for structure. At least it started out as "define a common style for a thing", and then if you really wanted to make a small deviation you could inherit and override something from the class.
Could be that since I'm not a frontender I just don't get it, but to me it feels like "global variables everywhere".
It can get messy pretty fast but I find once you sorta get a handle on the frameworks, it becomes easy to parse as long as people aren't doing 10+ classes per element.
I've found working without a CSS framework, people will tend to re-invent the wheel 100 times. i.e. I've seen "padded-box" and "box-with-padding" classes.
I experience the worst of the worst tho, as I do a lot of refactoring work, so my take is a bit bias haha.
Even the best design system needs local overrides to satisfy a product owner's incomprehensible requests, like "can we push that button a few pixels to the left?".
Note however that this API is brought by Styled System [1], which Chakra uses (and exposes), and adds the component library and other niceties on top.
My only regret with Chakra is that it's a tad runtime-heavy, and it's not really adapted to static content. I'd love to see some sort of compiler that digests a React tree into rendered HTML + CSS, with minimal JS just for style interactions (is this what Svelte does?)
In a design system. You specify variants of a component. Each one has its own combination of style defined by the design system.
You do not set „Gap“ to 2. you would specify smth like „small“, „medium“ or „large“ as props.
This is not a tailwind issue.
Are you unfamiliar with having-a-boss?
On the places where things work, bosses act like the GP's one. Granted, that doesn't happen on the large majority of places.
How would you handle a design change where a size becomes required that fits between "small" and "medium"? These things happen throughout the lifetime of a project.
Opaque numerical design tokens may not be the most explicit to understand, but they allow for expansion.
That's true.
> as this really has nothing to do with Tailwind
Well.. it has to do with using Tailwind with React for a design system. :)
He does mention that @apply doesn't fix the problems he mentions, which is what Tailwind suggests as the solution for abstraction when you don't use a component framework: https://tailwindcss.com/#component-driven (scroll down to "Not into component frameworks?")
The only real limitations with CSS imho was just lack of variable support for easily passing in variables to set color schemes and/or dimensions etc.
These seemed to be readily fixed with SCSS and the various options for embedding CSS in JS using styled components and the like.
Tailwind seems to be all about making it easy to “pixel f*k” your way to getting the design you want in one given place at the expense of having consistency and maintainability.
E.g. with a proper CSS template I can ensure all fonts and sizes and colors are consistent across an app. With Tailwind everything starts to be like the 1990s where all your design was hard coded into table and font elements mixed into your markup!
This does NOT seem like progress!
Having a standard library of utilities makes sense because if you don't use one you end up writing the exact same thin since they are mostly one liners.
What I don't understand is why you'd want to build a design system component out of utilities much less build everything out of utilities.
For app-like websites CSS cascade offers little and is often harmful as an innocent change to a top layer will unpredictably cascade down to everything below.
> at the expense of having consistency and maintainability.
Compared to what. Every single website devolves into a nightmare of incomprehensible class names, or one-off CSS-in-JS solutions everywhere.
Compared to that the actually consistent names enforced by Tailwind are both consistent and maintainable.
In most cases, it is faster to use Tailwind than customizing an existing UI framework.
SCSS and CSS in JS are more complex solutions than Tailwind.
Maintainability is generally better with Tailwind because you don’t need to remember all of your abstractions and any hidden structures, eg this div.className always needs to be nested a certain way. Onboarding is trivial because Tailwind can be mastered in two afternoons.
Tailwind might not be for everyone, but the features it provides allow for rapid development and easy maintenance. The author has issues with Tailwind in React, but these seem mire like React complexity than Tailwind.
* You don't know what you're doing * You're doing it wrong * You're an idiot.
It really kinda feels like the old AngularJS 1.x days as those were the typical responses to anyone who didn't fall down and worship it as a framework masterpiece. I've decided for myself, I'll sit this one out. Judging by history, most of the things we were fanatical about at first, we tend to look at with disdain in a few short years. jQuery, AngularJS, Bootstrap, Redux... It would be foolish to think this library/framework won't go the same way.
If you use it, and it works for you and your team, that's great. I would never try to tell you NOT to. For me, I'd rather not.
In most organizations I’ve been in components are used in applications that share a brand identity/style. Even if there are different brands consistency matters for each brand’s look/feel.
If styling is to be consistent then it’s way more maintenance to change each component to reflect branding changes than it is to have the brand/house style defined in one place and passed into the components.
E.g. decide that all the outlines around inputs, certain boxes etc. are going to be wider - that’s something you probably want to be able to change in one place not 20 or 100 down the line. Sure you “could” find and replace for some stuff but that could easily match the wrong stuff if you use tailwind on something big/complex… at least that’s my concern looking at it for stuff beyond quick prototyping.
I think you have a point. I think the cascading part was ignored for containment purposes during authoring, so it would scale and be predictable for larger teams. But ideally, I think we'd want to author using something like utility classes (or repeated style props?), but then compile them away to abstractions that cascade accordingly during runtime (least amount of kB sent over-the-wire as there would be as few as possible duplicated classes in the HTML, but also presuming that browsers using the cascade is more runtime performant than applying lots of atomic utility classes..?). Then we'd get abstractions without the cost of dealing with author-time abstractions (and having to name them). But then again.. there would then be a disconnect between what CSS classes you see when you author and what you see when you inspect the HTML/DOM... At least the utility classes are 1-to-1...
*runs for cover*
But CSS in JS, SCSS/SASS etc. take the things that work about CSS and add variables etc. to give you something that's useful in the modern world while also giving you a way to keep things maintainable.
I worry Tailwind is kind of like the 'fast food' for styling. Tastes good in the moment and satisfies the need for quick calories but ain't gonna be healthy in the long term especially if you do too much of it.
In my experience cascading is simply not a great idea even in itself — you can’t reasonably share part of your design between different components, it causes way too close coupling, breaking some non-intended component on some other page. What can reasonably be shared is variables and a color palette, which you can specify at a single place with tailwind.
But my point, design seems to primarily think in components - so just create a component in your framework/webcomponent, design it locally (e.g. by tailwind) and reuse that widget where you want.
Keep the local styling for the component local to its place in your repository - absolutely. Give it sensible defaults - for sure. But if you’re using it in something complex where the overall design may evolve it’s a maintainability nightmare to hardcode the styling at the component level and/or designing by classname.
I get why initial development is faster BUT… if you want to make a simple style change on a 100 component site - do you really want to be editing a load of components for every change down the line?
Just following the idea that making life easier for yourself six months from now when you don’t have the current project “context” in your head is way better for lowering technical debt.
Oh and we will probably be into another fashionable styling framework by then! ;-P
And this applies to any utility-based CSS framework.
Some people like this better. It reminds me of people that used to adjust the format of every single text area on Word instead of using styles. And on some contexts, that's even objectively better.
But most of the time it's just a bunch of developers arguing against generalization and encapsulation. I don't understand it either.
Anyway, just to add a bit:
> The only real limitations with CSS imho was just lack of variable support for easily passing in variables to set color schemes and/or dimensions etc.
Nowadays CSS has variables support that you can use for that.
Also this is weird:
``` const Card = (props) => { const className = "p-" + props.gap.toString(); return <div className={className} />; }; ```
Why do this? If gap needs to be set, then break apart the Card subcomponents (Card.Title, Card.Description, Card.Footer) and let the consumer handle their odd logic that break the design system guidelines.
I have faced issues with Tailwind too, but I would pick this 10 out of 10 times over styled-components and such.
`const classes = [some, classNames, here, foo ? 'foo' : ''].join(' ');`
On a related note, the article's example of why Tailwind is clumsier for a Box component misses out on the addition of the `space-y-{number}` and `space-x-{number}` class names, which are equivalent to the "gap" props in its React example. But I think those didn't exist in March 2021 so I don't blame the author.
I thought the main thrust of Tailwind is that you get a sensible set of utility classes so you can mix and match them how you need? For more complicated design systems can't you combine these utility classes into your own classes in a CSS file? You still retain the advantage of easier to read CSS and easier to read JSX.
I am always blown away with what raw css can do and would like to learn it, but it doesn’t click for me. When I first heard about tailwind I thought it was awesome sounding as I don’t jive with css, but when I tried it I was dismayed to have YET ANOTHER build step and thing I had to screw with and configure outside of the src code.
I would like to know why @apply does not solve the issues in the author's opinion. This is exactly the part where the author should have gone into details.
> 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.
So he would probably say that @apply just abstracts away the problem but it still exists somewhere that you'd need to parse through to understand the styling.
But this doesn't resonate with me. This isn't avoidable in any scenario. Either you have a styled component with a bunch of css-in-js, or a bunch of css, or a bunch of utility classes. In all scenarios there is an implementation that you have to parse. Which basically means it has nothing to do with @apply and everything to do with css vs. utility classes.
But of course, there are situations where it becomes unwieldy. I'd wager that a lot of the problems people have with Tailwind are more social in nature. Which are valid problems! It just means that you need some other solution for that. Analogously, many of the benefits of static typing are social in nature; notably in enforcing interfaces between teams. I suspect people are looking for a similar thing for CSS. But that doesn't invalidate the use cases where Tailwind is particularly handy.
They don't ever claim to be component-driven. They are utility-class library and nothing else.
I highly recommend using twin.macro if you are using React. It basically combines tailwind with styled components and helps immensely with readability:
That being said, when looking at Tailwinds problems, you have to ask yourself “compared to what?” Especially that first complaint - Tailwind is hard to change compared to…Bootstrap? Foundation? BEM? MUI? It’s vastly, vastly easier to change Tailwind code than any of those frameworks (IMO).
I’m a Tailwind champion not because it’s the perfect solution, but because I’ve found it to be better overall than anything that came before it.
The author compared it to Chakra UI (in the last code sample; misspelled Charkra).
He recommends ThemeUI, Rebass, Stitches and Radix for a design systems, specifically. A recent and very powerful alternative is Tamagui which takes inspiration from all of those.
One nice thing is that it does so in a way that allows for avoiding doubling the depth of your component tree by having to do HOC type solutions as many seem to do to work around them.
How?
The very first example in Tamagui docs is this:
export const Circle = styled(Stack, {
backgroundColor: '$background',
color: '$color9',
borderRadius: 100_000_000,
variants: {
pin: {
top: {
position: 'absolute',
top: 0,
},
},
size: {
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
It's already worse than Tailwind. And the rest is just a bunch of predefined components that you can do with any library/framework/vanilla CSS.If you just want the simple Tailwind experience you can use shorthands only in those, or even closer just import <Stack /> or <Text /> directly and use shorthands with typed tokens. It's there in a few of the first examples.
The upside there is they are just regular props that are typed and have object de-structure and re-structure.
import { Stack } from 'tamagui'
<Stack p="$2" mx="$1" bc="red" />In one of my prev companies, I was able convince my engineering manager to use tailwind alongside Antd and it worked flawlessly.
Just to make a point here, Antd is an example of design system and I used tailwind as a "utility" from which I can use vast amount of classes without writing seperate custom css for each of my components.
But a recent and very powerful alternative is Tamagui which takes inspiration from all of those. All for the cost of some 20-27kB, apparently with a clear path to come below 8 kB in the future: https://tamagui.dev/blog/version-one#bundle-size-reduction
It even has a tooltip feature, using floating-ui, which one may or may not add, and seems to come in at around 23 kB (in excess of tamagui core, when overlapping sub-libraries in @floating-ui/react-dom-interactions are discounted)... https://bundlephobia.com/package/@tamagui/tooltip@1.0.5 It's still a bit, but fortunately it's not in core so it's optional to include.
const className = "p-" + props.gap.toString();
according to https://tailwindcss.com/docs/content-configuration#dynamic-c...Stopped reading there. It's a utility library, that much has always been blatently clear and obvious. Really poor article.
backing: https://twitter.com/davesnx/status/1329407408922370050
claim/thread: https://twitter.com/davesnx/status/1329392089189265408
> Component-driven: Worried about duplication? Don’t be. ...
Which is entirely different thing than saying "Tailwind is component driven".
We don't use Tailwind directly on your components (unless necessary and for adjustment only, more on this later).
We try to keep Tailwind as an internal implementation detail. The consumer of our components should pass options as `variant="primary"` where `variant: "primary" | "secondary";` BUT it's okay to allow some Tailwind classes for customization on edge cases.
Example (by memory, syntax might be wrong):
import cn from 'classnames';
// Definition
function Button({ fill, isLoading, className }: ButtonProps) {
return (<button className={cn(
'relative',
'inline-flex',
'rounded',
{
'w-full': fill,
'opacity-50': isLoading,
},
className,
)}
>
{children}
</button>
);
// Usage
// Full-width button, with some margin on the left
<Button fill className={"ml-2"} />
// Regular button, with loading state
<Button isLoading />
You can even go further and use https://github.com/crswll/clb and create your own rebassjs.For reference, this is how Tailwind suggests using it with components (aka. "component-driven" as it says, which the author takes issue with): https://tailwindcss.com/#component-driven
I think the gripes come from trying to combine the bottom-up approach of Tailwind with the top-down approach of props based styling that JS component libraries typically allows. His point being that Tailwind does not work too well with such solutions for dynamism / contextual style overrides. You could solve it with using clsx or cva though, as seen in this video:
"Tru Narla: Building a design system in Next.js with Tailwind" https://www.youtube.com/watch?v=T-Zv73yZ_QI
The best way seems to be making styling a completely internal component concern, and not take in style props but simply semantic props like isActive=true and then have the component itself apply styles based on that, like NavItem.js on the Tailwind home page suggests: https://tailwindcss.com/#component-driven
This practise is elaborated in this example, that has the same button styled differently by passing in different semantic props: https://youtu.be/T-Zv73yZ_QI?t=570
While I like the css-in-js way of doing things, React seems to be moving away from runtime css generation, and I'm not sure the ecosystem will catch up (and I'm tired of playing catch up).
Sticking to CSS guarantees you won't have any compatibility problems.
Unless you attempt to go cross-platform.. There are many attempts at getting CSS working on React Native for instance, and many have various compatibility problems (especially as they need to keep up with the evolving CSS spec).
But if you do go cross-platform, then something like Tamagui is probably a better bet than trying to replicate CSS. You could also try Nativewind, to get the Tailwind benefits (and limitations, like lack of support for animations).
Tamagui is on my radar, but it's too new for me to give it a try with my limited time budget.
I thought Tailwind shined the most when used within components (and why it's become so popular with React).. making components and not CSS the nexus of abstraction.
> and instead of bloated packages like radix ui I think it's a more sensible solution. ... I think it is impossible to find an optimal way with the libraries it recommends.
The author recommends ThemeUI, Rebass, Stitches and/or Radix for design systems. They might add undue bundle size, to various degrees.
But a recent and very powerful (close to as optimal as possible?) alternative is Tamagui which takes inspiration from all of those. All for the cost of some 20-27 kB, apparently with a clear path to come below 8 kB in the future: https://tamagui.dev/blog/version-one#bundle-size-reduction
The best way seems to be making styling a completely internal component concern, and not take in style props but simply semantic props like isActive=true and then have the component itself apply styles based on that, like NavItem.js on the Tailwind home page suggests: https://tailwindcss.com/#component-driven
This practise is elaborated in this example has the same button styled differently by passing in different semantic props: https://youtu.be/T-Zv73yZ_QI?t=570
I love Tailwind and use it daily, have done for almost 6 years now and I haven't experienced any issue I couldn't solve
It certainly has never got me thinking to just throw it away or denigrate it publicly. And my solutions have worked well in a team setting - not adding any burden to other devs
This feels more like it's at the intersection of React-specific problems and lack of experience with Tailwind and/or CSS in general
I can see some of these problems being frustrating, but because the author seems to just be sounding off without much effort to express attempted solutions, I'm flagging because I feel like this is just anti-Tailwind inflammatory BS
Edit: Title also needs to indicate this is from 2021
On attempted solutions, there was a subsection titled: "What should I use instead of Tailwind for my design system?"
He also suggested this solution for Tailwind, if you read it carefully: "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."
The post was arguably not inflammatory, but pointed to specific issues with Tailwind viz-a-viz design systems. The points raised may not be correct(?), but that doesn't mean it's inflammatory BS.
> Edit: Title also needs to indicate this is from 2021
Good point. Updated.
You don't need the month, the HNconvention just uses the year in parens and some things rely on that format.
I think if it was really intent on providing a clear argument, the author would have gone to a little more trouble to show their work and taken us all on the journey
As they didn't, I'm not convinced, and the fact that it's made its way to the top of HN suggests there's just a bunch of anti-TW up-voters ready to jump as soon as there's a whiff of some apparently-new reason not to use it
Kinda disappointed this article got shared again tbh