Design systems after Tailwind

Olex
8 min readOct 25, 2021

--

The good ol’ box model

From 2009, I’ve spent years building simple design systems for my less CSS-savvy coworkers to use across Thinkful, Attune Insurance, and dozens of smaller projects. The fine minutia of buttons, inputs, grids, loading states, and default typography was my domain as much as any designer on the teams I’ve been on. So by the time Tailwind dropped in 2017, I had written a lot of CSS. And my first reaction to it was universally negative.

I’ve ripped out overlapping and confused utility classes out of dozens of sites in my experiences through 2010–2017: “utility classes” were a pariah that tripped up developers, not something to build a framework around. The shorthand nature of tailwind didn’t attract me either. The combination of vim, emmet-style expansion, and linters to standardize ordering meant that I already had a great experience reading and writing CSS. If you’d asked me in 2017 if I thought Tailwind would be a thing in 2021, I’m sure I would scoff and say “no way!”

But having worked with a couple sites that use Tailwind in the past couple years, I wanted to write down what I think people love about Tailwind and show how you can incorporate these ideas into any design system. If you, like me, threw out the baby with the bathwater, I encourage you to take another look at Tailwind for inspiration.

Generic lengths and unit systems

The discourse around modular scales and baseline grids had existed for years in the type design corner of the internet, so its presence in Tailwind didn’t feel super novel. Bootstrap 4 had exactly the same margin & padding utilities, but I think we have to give credit to Tailwind for opening up unit systems to a broad audience and getting the details right [See footnote].

To a lot of developers, the fact that something like a bigger bottom margin was expressed as mb-5 instead of mb-4, and not some value like 6.25rem, must’ve been a breath of fresh air. The units part of Tailwind went a little further than any typographic unit systems of the time, and it paid off. Nudging designs feels great when you have quick access to the next-smallest or next-largest unit.

Nowadays, I think it’s a given that a complete style system has a set of lengths to use in a lot of contexts, primarily for layout. If you have to think about “a little bigger” or “a little smaller”, and convert that to pixels or rems day-to-day at work, you’d benefit a lot from standardizing lengths into your style system.

→ Take-away #1: Design systems for the web should ship with a unit system for length values. A lot of folks have adopted multiples of 8px, or their equivalent in rem, but a musical / modular scale works just as well. Another term for this is spacing tokens.

Utility classes for margin and padding nudges

Following strict BEM-like rules for CSS, you end up writing a bunch of modifier classes for a lot of elements. For button classes you need button--no-margin to set margin: 0 on buttons that have specific placement. Even if you instruct your coworkers to go ham with specific naming for modifiers (button--students-top-section), these end up reused and composed just like utility classes, with a minor benefit of specificity, and major downsides in verbosity. These classes are fighting with each other at the “2 classes” level of specificity instead of a single class, but they’re just as messy as utilities.

Tailwind’s approach, e.g. putting a global utility mb-0 on a global button, eliminates the need for some all-too-basic modifier classes. Whoever is tending to your CSS stack still has to worry about order-of-imports, but for the most part, these types of layout tweaks should be utilities. They should successfully override the classes of core styleguide elements like type elements, buttons, forms, and so on. This reduces pollution for common elements, so all the real modifier classes are meaningful variants, like button.button__primary and button.button__transparent.

→ Take-away #2: Design systems for the web should ship with margin and padding utility classes. The canonical shorthand is m for margin: mt, mr , mb , ml , mx , my & the p for padding.

Effects and t-shirt sizes

Tailwind’s pattern around shadow classes to apply outer shadows is excellent, and for me it was the part that took the longest to internalize. I said to myself: This is cool, but I already know how to freehand a great box-shadow using the standard syntax and a color variable, I don’t need this. For complex stuff, you could use mixins. Only after actually working with Tailwind did I realize there was something else at play here. The experience of applying shadow and tweaking it using utilities is just easier. The naming convention of shadow for the style system’s default box-shadow, shadow-xs for less, shadow-xl for more, is super intuitive.

In a typical in a design system a lot of visual flair specific to a brand ends up expressed in just one of a dozen ways in CSS: borders, box-shadows, background properties, filters, transforms, pseudo-elements, text properties, and combinations thereof. In the case of shadows, this maps directly with the Figma or Sketch concept of layer effects, so I’ll refer to these as “effects,” but keep in mind it’s a broad term that might be implemented differently.

Effects in general are great candidates for utilities. These utilities rarely interfere with the things that are useful for layout.

Chances are, it’s not just this one embellished element that needs this particular kind of shadow effect. It’s not just this one header that needs a weird angle and a drop letter. In a style system, these effects need to be reused, and with varying degrees of intensity. To achieve the same colorful shadow effect on an h3 rather than a huge h1, the values need to be smaller. In some systems, maybe numeral values (like shadow-5 ) would make sense, but for the most part effects have a smaller range of useful sizes, and the T-Shirt size scale that Tailwind uses for shadows (xs, sm, <no-suffix default>, md, lg, xl, 2xl) is a good way to go.

Mixins named this way are the more traditional way to handle effects, and exposing a mixin like shadow-xl alongside the utility class is definitely a good idea. In most cases, however, effects are orthogonal to what you need custom CSS for, and leaving the shadow as a utility class on a custom element can be totally appropriate (think class="newsletter-header cool-bg-xl"). In fact, having a fancy 5-line background effect in a separate class can help readability when inspecting and debugging.

→ Take-away #3: Design systems should express reusable visual flair as ranges of utility classes and mixins. T-Shirt sizes are a good way to express smaller-than- or larger-than-default effects.

Doing this in your own style system

To give you an example, let me show some signs of Tailwind influence that have crept into a recent project of mine.

1. The unit system:

:root { /* 1rem = 10px by default */
/* ... */
--unit11: 4.5rem;
--unit10: 3.33rem;
--unit9: 2.465rem;
--unit8: 1.825rem;
--unit7: 1.35rem;
--unit6: 1.35rem;
--unit5: 1rem;
--unit4: 0.74rem;
--unit3: 0.5475rem;
--unit2: 0.405rem;
--unit1: 0.3rem;
--unit: 0.3rem;
--unit0: 0;
}

This isn’t linear, so the difference between var(--unit1) and var(--unit2) is around 1px, whereas bumping from var(--unit9) to var(--unit10) is around 9px. The lack of a hyphen between “unit” and the number is admittedly an affordance for my personal setup in vim—where Ctrl-A increments the nearest number, etc, and the hyphens make vim think these numbers are negative.

This unit system has a trade-off compared to Tailwind. In Tailwind unit values add up (a pt-10 and pt-6 add up to the value of pt-16), but there’s no pt-7 to keep the number of classes sane. Using a multiplicative / musical scale, there’s no missing unit values, so unit7 exists, but some baseline grid alignments can be more challenging.

2. The utility classes:

All of the margin and padding utilities can be generated in a CSS preprocessor loop. In this case I’m using the plugin postcss-each, and showing just the utilities that affect margin-top in the order they appear:

@each $i in 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 {
/* ... */
.m-$(i) {
margin: var(--unit$(i));
}
.my-$(i) {
margin-top: var(--unit$(i));
margin-bottom: var(--unit$(i));
}
.mt-$(i) {
margin-top: var(--unit$(i));
}
/* ... */
}

Important note: The order of lines and imports matters a lot for this snippet. My personal preference is to put it at the end of the “styleguide” or generic styles, but before any of the application code. For example, consider this
<h2 class="h1 mt-1">Big header</h2>: css specificity takes care of the class h1 overriding the styles of the default h2, but you need to be careful with import order such that mt-1 overrides oft-used classes.

Worried about the amount of code it’s generating? So was I! Luckily, all the same tools to reduce Tailwind’s impact apply to this, there’s plenty of static analysis tools that strip out unused classes for production. Additionally, gzip compression is on by default everywhere and it’s excellent at compressing repetitive code like this.

For custom elements, I tend to hand-write margin: var(--unit2) var(--unit3); and so forth, but if you prefer @mixin my-2, @mixin mx-3, you could export mixins from the same loop above.

3. Effects and t-shirt sizing:

Whenever you’re introducing a layering effect like a shadow, or a colorful branding gimmick to your style system, you should consider creating a range of these from xs to xl.

The key here, in my mind, is that this isn’t just about shadows. Here’s an example of how Gap might’ve applied this if they kept their oft-mocked branding, using a pseudo-element:

<h1 class="bluebox-xl">Gap</h1>
<h2 class="bluebox-lg">Gap</h2>
<h3 class="bluebox">Gap</h3>
<h4 class="bluebox-sm">Gap</h4>
<h5 class="bluebox-xs">Gap</h5>
Multiple sizes of a visual element using xl, … xs, CSS classes

I’ve put the CSS source code in a Codepen here.

If these types of effects take care not to mess with the layout attributes, they can be pretty safe to use as utility classes, and should always be available as mixins for more complicated use-cases.

Conclusion

I’m hoping to find a lot more style systems embracing unit systems for spacing, utilities for nudging specific properties, and t-shirt sized effects. We all have Tailwind to thank for popularizing these things, even if we disagree on the rest.

Footnote about Bootstrap4

It’s very likely that mt-1 and similar utility classes were conceived in Bootstrap 4 alpha / beta, a year+ before Tailwind, even though the release version of v4 launched a few months later. Boostrap5 also led the way in adopting ms and me instead of ml and mr, — e.g. start instead of left, end instead of right, to support RTL styling by default.

Bootstrap’s use of !important makes sense for its context (something you import from CDN, can’t guarantee HTML order), but home-spun style systems should avoid it. Also, I’m reusing tailwind’s CSS order in the margin utilities example. Hence, tailwind “got the details right” on these sorts of utilities.

🔌 That’s all for this post, thanks for reading! I run a small design-adjacent development studio over at 🍕 team.pizza 🍕 specializing in style systems and challenging frontend work. You can find me on twitter and elsewhere.

--

--