4

Don't use Tailwind for your Design System

 1 year ago
source link: https://sancho.dev/blog/tailwind-and-design-systems
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Don't use Tailwind for your Design System

March 2021

or your UI Framework or component system, call it whatever you prefer. Tailwind isn't component-driven, which they claim to be, and you might struggle making a Design system with it, let me explain it in this post.

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 the drawbacks to make a Design system with Tailwind, with focus on the technical part of the components. I'm assuming that the reader is familiar with Design systems, React and basic programming. Otherwise, can read more about in here or here.

A little bit about my experience

At the time of writing I'm working at Draftbit and our frontend is build on top of ReasonReact using Tailwind. It's about ~500 components and the main page, called the builder looks like this:

screenshot-draftbit-builder

Aside from Draftbit's design system 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 the patter of utility classes. This pattern 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.
  • Just CSS It's not an abstraction, or coupled to any framework. It can be used with minimal setup. All IDE would have support for it, all frontend/fullstack frameworks can integrate easily, there's ton of build tools to optimise performance, because it's just The Platform makes the adopation easier and future-proof.

How is used in React

I use React as example, but it's similar to other component-based UI libraries. In particular, React uses JSX via Babel (or any transpiler) which to transform JSX to function calls and eventually will endup as DOM Elements.

During this transformation className gets renamed to class (since it's a reserved keyword in JavaScript) which applies CSS classes to 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

Editor-with-classNames

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. They can collide and override with other properties.

There're a lot of tools to solve that particular issue, such as linters or editor plugins. But it's still a problem.

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: when your component is made by nested elements with many Tailwind utilities you aren't capable to safely refactor your structure.

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.

screenshot-jsx-sketch-UI

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 makes your components re-usable across all your codebase and can be dependent to a global theme. Providing those versions of the same component available to their user. 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. Allowing those values to be driven by Tailwind it's a complexity on top that I found 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

To give a simple example: If you want to create a component with a prop like gap. gap accepts all the possible spacing values that Tailwind does, let's see how we need to do that:

const Card = (props) => {
  const className = "p-" + props.gap.toString();
  return <div className={className} />;
};
 
<Card gap={3} />;

Now our Card component accepts the value for padding. This have a few issues by itself. Such as gap could technically be any string, like "4 flex" and it will break all the UI. If you abuse the dynamism inside your components can be a real pain to implement the intermediate logic between your component API and your Tailwind utility classes. That dynamism is often needed while making your Design system.

Impossible to derive styles

derive styles are a nice usage of JavaScript values to generate scalar UI. Since Tailwind uses those utilities it's very tempting to use them for your components, making derive impossible.

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

Shaper-by-hayk

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.

Stack-documentation-taco

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.

Card-from-CharkaUI

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. Since it's a static definition, you would only abstract those lists into a CSS file. I don't want to get into much details about it, but it does not solve the problems mentioned before.

When I would use Tailwind again then?

  1. 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.
  2. 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.

Thanks for reaching the end. Let me know if you have any feedback, correction or question. Always happy to chat.

If you like it enough, consider to

© 2022 David Sancho

Source


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK