Using Tachyon's Design System

Tachyons is a pretty simple (build system-less) utility CSS library, and I quite like its design system. As far as I can tell, it has mostly "lost" against Tailwind and doesn't see too much use. However, that doesn't mean I can't still use it to demonstrate how we can use the design system of a utility library to make writing CSS simpler.

The component we will be recreating is the first banner component on the Tachyons components page. The HTML for the component is as follows:

Tachyons banner component
<article class="mw7 center ph3 ph5-ns tc br2 pv5 bg-washed-green dark-green mb5">
  <h1 class="fw6 f3 f2-ns lh-title mt0 mb3">
    This is a tagline. For x.
  </h1>
  <h2 class="fw2 f4 lh-copy mt0 mb3">
    This will change things. And we want you to be involved. This text needs to
    be longer for testing sake.
  </h2>
  <p class="fw1 f5 mt0 mb3">
    Sign up for beta access or learn more about x.
  </p>
  <div>
    <a class="f6 br-pill bg-dark-green no-underline washed-green ba b--dark-green grow pv2 ph3 dib mr3"
      href="#">
      Sign Up
    </a>
    <a class="f6 br-pill dark-green no-underline ba grow pv2 ph3 dib"
      href="#">
      Learn More
    </a>
  </div>
</article>

As you can see, it is not that unreadable but I still don't like every CSS property being its own class. To me, this defeats the point of using CSS which to some people is the point of using a utility library. I recommend reading Josh Collinswirth's article on the matter.

Naming things is supposedly hard but of course we already know what this component is called: it's a banner. This is how I've changed the HTML:

Tachyons banner component (converted)
<article class="banner">
  <h1>This is a tagline. For x.</h1>
  <h2>This will change things. And we want you to be involved. This text needs to be longer for testing sake.</h2>
  <p>Sign up for beta access or learn more about x.</p>
  <div>
    <a class="btn btn-filled" href="#">Sign Up</a>
    <a class="btn" href="#">Learn More</a>
  </div>
</article>

Very minimalistic but I don't think every single part of a component should have its own class. Sibling and child selectors are part of CSS for a reason.

To start implementing our component, we first have to extract the design system. This is basically the whole point of this write-up. After going through the Tachyons documentation, I've made the following CSS variables:

Tachyons design tokens as CSS variables
/* TACHYONS DESIGN TOKENS */
/* sourced from https://tachyons.io/docs/ */
:root {
  /* TACHYONS COLORS */
  --color-black: #000000;
  --color-near-black: #111111;
  --color-dark-gray: #333333;
  --color-mid-gray: #555555;
  --color-gray: #777777;
  --color-silver: #999999;
  --color-light-silver: #AAAAAA;
  --color-moon-gray: #CCCCCC;
  --color-light-gray: #EEEEEE;
  --color-near-white: #F4F4F4;
  --color-white: #FFFFFF;
  --color-dark-red: #E7040F;
  --color-red: #FF4136;
  --color-light-red: #FF725C;
  --color-orange: #FF6300;
  --color-gold: #FFB700;
  --color-yellow: #FFD700;
  --color-light-yellow: #FBF1A9;
  --color-purple: #5E2CA5;
  --color-light-purple: #A463F2;
  --color-dark-pink: #D5008F;
  --color-hot-pink: #FF41B4;
  --color-pink: #FF80CC;
  --color-light-pink: #FFA3D7;
  --color-dark-green: #137752;
  --color-green: #19A974;
  --color-light-green: #9EEBCF;
  --color-navy: #001B44;
  --color-dark-blue: #00449E;
  --color-blue: #357EDD;
  --color-light-blue: #96CCFF;
  --color-lightest-blue: #CDECFF;
  --color-washed-blue: #F6FFFE;
  --color-washed-green: #E8FDF5;
  --color-washed-yellow: #FFFCEB;
  --color-washed-red: #FFDFDF
  /* TACHYONS TYPE SCALE */
  /* NOTE: why is it from large to small??? */
  --font-headline: 6rem;
  --font-subheadline: 5rem;
  --font-size-1: 3rem;
  --font-size-2: 2.25rem;
  --font-size-3: 1.5rem;
  --font-size-4: 1.25rem;
  --font-size-5: 1rem;
  --font-size-6: .875rem;
  --font-size-7: .75rem;
  /* TACHYONS LEADING (line-height) */
  --leading-solid: 1;
  --leading-title: 1.25;
  --leading-copy: 1.5;
  /* TACHYONS SPACING SCALE */
  /* NOTE: variable names changed to be more concise */
  --spacing-none: 0;
  --spacing-xs: .25rem;
  --spacing-sm: .5rem;
  --spacing-md: 1rem;
  --spacing-lg: 2rem;
  --spacing-xl: 4rem;
  --spacing-xxl: 8rem;
  --spacing-xxxl: 16rem;
  /* TACHYONS BOX SHADOW */
  --shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 );
  --shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 );
  --shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 );
  --shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 );
  --shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 );
  /* TACHYONS BORDER RADIUS */
  --radius-1: .125rem;
  --radius-2: .25rem;
  --radius-3: .5rem;
  --radius-4: 1rem;
  --radius-100: 100%;
  --radius-pill: 9999px;
  /* TACHYONS FONT FAMILIES */
  --font-sans: -apple-system, BlinkMacSystemFont,
               'avenir next', avenir,
               'helvetica neue', helvetica,
               ubuntu,
               roboto, noto,
               'segoe ui', arial,
               sans-serif;
  --font-serif: georgia,
               times,
               serif;
  --font-mono: Consolas,
               monaco,
               monospace;
}

I've made more variables than necessary for this example, but I've already extracted them in case I (or other people) want to use them in the future.

Now we can start implementing our component. Basically, we just go through and turn every utility class back into a CSS property. For example: bg-washed-green -> background-color: var(--color-washed-green);. In the case of Tachyons, each component also has the used utility classes on the page so it's extra easy.

After converting every property, I've ended up with the following:

Tachyons banner component conversion
/* TACHYONS BANNER CSS */
/* based on https://tachyons.io/components/banners/basic/index.html */

.banner {
  /* mw7 center ph3 ph5-ns tc br2 pv5 bg-washed-green dark-green mb5 */
  max-width: 48rem;
  margin-inline: auto;
  padding-inline: var(--spacing-md);
  text-align: center;
  border-radius: var(--radius-2);
  padding-block: var(--spacing-xl);
  background-color: var(--color-washed-green);
  color: var(--color-dark-green);
}

.banner > h1 {
  /* fw6 f3 f2-ns lh-title mt0 mb3 */
  font-weight: 600;
  font-size: var(--font-size-3);
  line-height: var(--leading-title);
  margin-block-start: 0;
  margin-block-end: var(--spacing-md);
}

.banner > h2 {
  /* fw2 f4 lh-copy mt0 mb3 */
  font-weight: 200;
  font-size: var(--font-size-4);
  line-height: var(--leading-copy);
  margin-block-start: 0;
  margin-block-end: var(--spacing-md);
}

.banner p {
  /* fw1 f5 mt0 mb3 */
  font-weight: 100;
  font-size: var(--font-size-5);
  margin-block-start: 0;
  margin-block-end: var(--spacing-md);
}

.btn {
  /* f6 br-pill dark-green no-underline ba grow pv2 ph3 dib */
  font-size: var(--font-size-6);
  border-radius: var(--radius-pill);
  text-decoration: none;
  border-style: solid;
  border-width: 1px;
  padding-block: var(--spacing-sm);
  padding-inline: var(--spacing-md);
  display: inline-block;
  background-color: var(--btn-bg-color, transparent);
  color: var(--btn-color, var(--color-dark-green));
  
  transform: translateZ(0);
  transition: transform 0.25s ease-out;
}

.btn:is(:hover, :focus) {
  transform: scale(1.05);
}

.btn:is(:active) {
  transform: scale(.90);
}

.btn-filled {
  /* f6 br-pill bg-dark-green no-underline washed-green ba b--dark-green grow pv2 ph3 dib mr3 */
  --btn-bg-color: var(--color-dark-green);
  --btn-color: var(--color-washed-green);
  border-color: var(--color-dark-green);
}

Like I said before, the CSS is arguably more difficult than before in exchange for the HTML being much simpler. Which version is better will depend on whether you prefer working in CSS or HTML.

I've also used child selectors like .banner > h2 because I don't think every single part of a component needs its own class. If needed, you can still use some utility classes (in combination with CSS layers to avoid specificity problems) to give certain sub-components a specific look.

This version of the code also has the benefit that CSS variables can be change dynamically so you can use localized CSS variables and switch the values in different themes or subcomponents. An example of this can be seen here.

The complete code for this "article" can be found here, along with some additional thoughts.