Design Tokens

Typography in Design Tokens for Figma and Code

In this post I share what I've learned working on the typography system for Cabana (a design system for Figma built on design tokens).

Jan Six
Mar 20, 2022
7 min read
Photo by SIMON LEE / Unsplash

In recent months I've gotten quite a few requests around the topic of typography in design tokens, how to make the most of them and how to integrate both into Figma and Style Dictionary.

In this post, I share what I've learned while working on the typography system for Cabana (a design system starter for Figma built on design tokens).


Definition

Typography design tokens are special. They're special, as they're so called composite design tokens. They're made up of many atomic design decisions, such as what font family is being used, what font weight or what font size is being used. But also, line height, text capitalisation or text decoration all come into play.

At a bare minimum, a typography design token should include font size and family. Optionally, and if we're going to be using these with the Figma Tokens plugin, we should also provide decisions for other properties such as weight, letter spacing or line height.

The words 'The quick brown fox jumps over the lazy dog' in white, with various styling property notices alongside it
A typography style is made up of many design decisions
{
  "token": {
    "value": {
	  "fontFamily": "Inter",
	  "fontWeight": "Bold",
	  "fontSize": "24px",
	  "letterSpacing": "-1%",
	  "lineHeight": "140%",
      "textCase": "none",
      "textDecoration": "none"
    },
    "type": "typography"
  }

Now with these decisions in place, we have enough information to create CSS style classes, Figma typography styles or any other transformation we could do with Style Dictionary. However, we might want to structure our typography decisions in a more atomic way.


Atomic typography decisions

Take a look at your existing design system and inspect the typography styles you have created. How many different choices of font family/weight combinations are there? How many font sizes are you using? It may make sense to perform an audit of those decisions, to understand why we made these choices and if some of these are even required.

Especially in multi-brand design systems, it's essential to be able to swap out decisions easily, without having to recreate all styles or css from scratch. What if we could build our typography tokens with reusable values, like a design token referencing another?

Instead of the code I've shown above, what if we were using reusable decisions instead? What if we wanted to create 4 typography design tokens, large/small both for headlines and body, all using atomic decisions so we could easily change any of them, without having to recreate the whole token?

An uppercase and lowercase 'A' in white with various styling property examples pointing towards it
By using atomic typography tokens we can extract all unique values used in a typography style
{
  "fontFamilies": {
    "default": {
      "value": "Inter",
      "type": "fontFamily"
    }
  },
  "weights": {
    "bold": {
      "value": "Bold",
      "type": "fontWeight"
    }
  },
  "sizes": {
    "sm": {
      "value": "12",
      "type": "fontSize"
    },
    "md": {
      "value": "16",
      "type": "fontSize"
    },
    "lg": {
      "value": "24",
      "type": "fontSize"
    },
    "xl": {
      "value": "32",
      "type": "fontSize"
    }
  },
  "ls": {
    "headlines": {
      "value": "-1%",
      "type": "letterSpacing"
    },
    "body": {
      "value": "0%",
      "type": "letterSpacing"
    }
  },
  "lh": {
    "headlines": {
      "value": "110%",
      "type": "lineHeight"
    },
    "body": {
      "value": "140%",
      "type": "lineHeight"
    }
  },
  "headlines": {
    "large": {
      "value": {
        "fontFamily": "{fontFamilies.default}",
        "fontWeight": "{weights.bold}",
        "fontSize": "{sizes.xl}",
        "letterSpacing": "{ls.headlines}",
        "lineHeight": "{lh.headlines}"
      },
      "type": "typography"
    },
    "small": {
      "value": {
        "fontFamily": "{fontFamilies.default}",
        "fontWeight": "{weights.bold}",
        "fontSize": "{sizes.lg}",
        "letterSpacing": "{ls.headlines}",
        "lineHeight": "{lh.headlines}"
      },
      "type": "typography"
    }
  },
  "body": {
    "large": {
      "value": {
        "fontFamily": "{fontFamilies.default}",
        "fontWeight": "{weights.bold}",
        "fontSize": "{sizes.md}",
        "letterSpacing": "{ls.body}",
        "lineHeight": "{lh.body}"
      },
      "type": "typography"
    },
    "small": {
      "value": {
        "fontFamily": "{fontFamilies.default}",
        "fontWeight": "{weights.bold}",
        "fontSize": "{sizes.sm}",
        "letterSpacing": "{ls.body}",
        "lineHeight": "{lh.body}"
      },
      "type": "typography"
    }
  }
}

Now imagine we didn't have only 4 styles. A more realistic scenario would probably be somewhere between 12 and 40 being used. Imagine having to change a font size or a font family across all of these without using a reference. It would take a considerable amount of time, whereas changing the styles with the code above only requires me to change the source of the reference:

An uppercase and lowercase 'A' in white, with two typeface variants shown, and with various styling property examples pointing towards it
Changing a large amount styles is easy if we work with atomic typography tokens
{
  "fontFamilies": {
    "default": {
      "value": "Roboto Slab",
      "type": "fontFamilies"
    }
  }
}

As I was only changing this atomic decision, it wouldn't matter if I had 4 styles or 40. As the reference changed, all typography tokens using this reference changes as well. By using Figma Tokens, all our styles that match the name would update automatically.


Dials and levers

By embracing this concept you effectively create a few dials and levers in your design system that allows you to easily turn the whole experience into another. A multi brand design system would define its core typography tokens once, only swapping out decisions for its various brands, much like in css where you'd define these variables and then reuse them as you go.

Brand A could use a playful fontFamily with a larger fontSize scale. Brand B could use a more serious font, a smaller fontSize and maybe a changed letterSpacing. It may make sense to introduce design tokens even to those properties that have been set to 0, as some of your brands might need to tweak those later on.


Cabana for Figma

For Cabana, the design system starter for Figma that Marc Andrew and I have been building, we've chosen to introduce a highly reusable and customisable typography system that contains a couple of design tokens making it easy to create and update Figma styles, just by changing the JSON.

Example of a font family style guide, with various text styles shown
Cabana comes with a highly customisable set of design tokens pre-defined.

There's headline and body fontFamily/fontWeight combinations so users can easily change their body or headline fonts just by changing a single token. This makes it especially easy to customise the system to your brands needs in seconds.

A fontSize scale that's fluid: By utilising a base value of 16 and a scale value of 1.2 we're creating all typography sizes based on math functions that include the previous fontSize multiplied by scale. This way, users only need to tweak the scale or the base value to change every size that's being used across our design tokens. If that's not your cup of tea, you could fall back to just using px values as well.

Letter spacing, text case and text decoration tokens for body, headlines, captions and buttons so users can easily change the look and feel of any of those just by changing any of these.

All typography design tokens are composed of these, meaning you could just change one to influence all those affected. Want your captions to be uppercase with increased letterSpacing? Easy, just change the letterSpacing.captions token to something like 5% and textCase to uppercase.

Example of a font family style guide, with various text styles shown
Swapping typography decisions in Cabana with the click of a button

If you're creating a custom brand or a multi-brand design system, you wouldn't have to recreate all those typography styles. All it would take to change all 40 typography styles to another font/weight combination that's using a different type scale and base font size is replacing the design token that was being referenced.


Automation with Style Dictionary

Now while using those typography design tokens to update your Figma styles is great, it would be even better if we could transform them to css variables or css classes. Good thing Danny Banks created Style Dictionary, which allows us to transform our design decisions to whatever we want to transform it. By setting up configuration once you'll be able to transform all your tokens to css variables or even css classes, allowing you to use those directly in a web project of yours. If you bought Cabana, we'll be publishing an official config soon, so keep an eye on your inbox for that.

This allows you to make changes to the Cabana design tokens in Figma and then transform them to css variables with the click of a button. You could even automate this by integrating an automated build process, triggering a rebuild of your css variables whenever something changes in the source tokens file.

One thing to consider is that you would need to have the font files that make up our typography decisions in the directory where your code is being served. An alternative to this could be using Google fonts, where you'd only be swapping out the font name being used to whatever's defined in the tokens JSON.

As of today, style dictionary has no support for composite tokens out of the box. Meaning, you can't create tokens for each of the properties stored on a typography token. Instead, you could create a custom transform that parses the typography token and outputs the font: shorthand that you could use in CSS.

StyleDictionary.registerTransform({
   name: 'typography/shorthand',
   type: 'value',
   transitive: true,
   matcher: token => token.type === 'typography',
   transformer: (token) => {
     const {value} = token
     return `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${value.fontFamily}`
   }
 })

Or, you could define a custom format that would output css classes for every typography style you have defined. You'd have to make sure those values are transformed correctly, though. As they won't pass through style dictionary's transform system.

StyleDictionary.registerFormat({
   name: 'css/classFormat',
   formatter: function (dictionary, config) {
     return `
 ${dictionary.allProperties
   .map((prop) => {
     return `
 .${prop.name} {
     font-family: ${prop.value.fontFamily},
     font-size: ${prop.value.fontSize},
     font-weight: ${prop.value.fontWeight},
     line-height: ${prop.value.lineHeight}
 };`})
   .join('\n')}
 `
   },
 })

Conclusion

Using atomic design decisions for typography allows us to customise only specific parts. It makes defining a typography system easier, more maintainable and easier to understand why we used certain choices. Connecting our source of truth to an automated build process means that designers can make these changes themselves, just by editing their tokens they're able to generate all artifacts such as css variables automatically.

By the way, if you want to really save yourself hundreds of design hours, my Design System for Figma; Cabana is now available.
Special Offer: Use the code CABANA35 to receive 35% OFF. 👇

Cabana - Design System for Figma
Creating amazing designs is hard when you’ve got deadlines to meet. So we built Cabana, a Design System for Figma that enables you to start projects faster.

Thanks for reading the article,
Jan.