tutorial0 min read

Creating a fonts package

Intro

Storing a centralized package for fonts in your design system allows you to leverage a source of truth for the basis of your typography.

This article will include some steps for exporting a fonts package that supports design tokens, allowing for quick visual refreshes and standardization for typography.

A bright yellow sheet featuring the word Typography in different languages

Where should it live?

It's important to keep this type of asset package outside of where you store your other design system components, because:

  • You want to make sure component changes don't bump your fonts version
  • Keeping it separate repo makes it easier to use across multiple repos
  • It can leverage a separate design tokens version if needed
  • You can simplify/customize the build process easily
  • A smaller scope reduces overhead

Project setup

Since this wouldn't be an app, we don't need to add too much to get started.

These steps included setting up some files/folders that I would configure later:

  • src - Where all the font-specific files live
  • .autorc - Helps automate workflows, check out auto
  • .editorconfig - For linting
  • .gitignore - Tells git what to ignore
  • .npmrc - Makes sure my registry is set correctly
  • Jenkinsfile - Defining the build steps for our CI (continuous integration)
  • README.md - Intro docs
  • package.json - Package info, scripts, dependencies
  • webpack.config.js - Our bundle settings

Initial support

For the sake of simplicity, let's focus on supporting one font from this package. Eventually this repo would be able to support additional fonts, so it's important we keep that in mind.

This meant we should probably create a folder with the name of the font in the src folder, and we can add additional folders for more fonts later on with their own corresponding packages if required.

Font formats

Within my named font folder, I added the font files themselves in WOFF2 and TTF formats. These files are meant to be used as a source, that can be uploaded separately to a CDN.

[font-name]
└───woff2
    │   [font-name-regular].woff2
    │   [font-name-italics].woff2
    ...

Note: Using something like fonttools, you could just use a single file source that handles all these exports for you, rather than having to store several different versions. This would require some testing to ensure the output is consistent across operating systems and browsers, but is ultimately more efficient and would lend itself to easier font changes in the future.

Static vs. variable font files

Static font files mean that for each font weight, you have a corresponding font file. There are a few reasons why you'd want to support them:

  • Not all browsers support variable font files (looking at you, IE11...)
  • You can assign your own number to the file name ("thin" --> 200)

It makes your CSS a little lengthy and manual have to define each one, and it lacks the flexibility that you get with a variable font.

The font weight range support for variable fonts is based on the internal weights of your original files. This is important because you no longer have control over what "500" means in your system.

While variable font files do support both regular and italics in a single file through the use of multiple axes, it's recommended to keep them separate for the web.

Font-face definitions

Now that we have our font files, the next step is to determine how we want users to access the font.

In order to make sure these files are being used instead of whatever may be installed locally on their machine, it's good practice to define the font-family with a custom name.

For this example, we're going to go with: Fonty McFont Face.

@font-face defintions are different from a regular selector, in that font-family supports just one name. This is because we're trying to assign it a name that can be referred to later.

Ideally, this name and its corresponding font-family (including fall-backs) come from design tokens, so that should these values change, nothing breaks by accident.

Design tokens

We can store the data we need for this by using two different tokens:

{
  "font": {
    "name": {
      "value": "Fonty McFontFace"
    },
    "family": {
      "value": "{font.name.value}, serif"
    }
  }
}

Make sure to export your design tokens in both .scss and .css formats, and that you add your design tokens package as a devDependency.

CSS variables through SCSS

A (not so) fun fact: CSS variables are not supported in @media or @font-face rules because CSS variables depend on :root. Currently, there is a draft to support this functionality using :env but this solution requires PostCSS for now.

That being said, there are two approaches we can use to support to import our design tokens: SCSS or PostCSS. We weighed the pros and cons of both, and ended up moving forward with SCSS since it required less setup.

To set up SCSS, we need to install the following devDependencies:

yarn add sass-loader style-loader css-loader node-sass mini-css-extract-plugin --dev

Then reference them in webpack.config.js:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [new MiniCssExtractPlugin()],
  module: {
    rules: [
      {
        test: /\.scss$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  }
}

Importing tokens into SCSS

We're now ready to define our @font-face rules:

// SASS import lets us reference tokens locally (within this file)
@import '@package/to/your/tokens.scss';
// CSS import allows users to import css variables (eg. using var(--font-family))
@import '@package/to/your/tokens.css';

@font-face {
  font-family: $font-name;
  font-style: normal;
  src: url('link/to/the/CDN') format('woff2-variations');
  font-weight: 100 700;
  font-display: fallback;
  font-synthesis: none;
}

@font-face {
  font-family: $font-name;
  font-style: italic;
  src: url('link/to/the/CDN') format('woff2-variations');
  font-weight: 100 700;
  font-display: fallback;
  font-synthesis: none;
}

Setting our index.js

Now that we have our font files defined, we need to add an index.js file outside of our named font folder that imports our .scss file:

import './fonty/fonty-mcfont-face.scss'

Exports and defining the package

Last but not least, we need to point our package so that users who import it will get access to our built, compiled CSS file that contains:

  • The @font-face rules
  • Design token CSS variables

We can do this in our package.json:

  "name": "@my-awesome-blog-post/fonts",
  "version": "1.0.0",
  "description": "The best fonts you've ever seen",
  "main": "./dist/main.css",
  ...
  "files": [
    "src",
    "dist"
  ],
  "scripts": {
    "build": "webpack --mode=production",
    "test": "jest"
  },

Note that our "main" points to the built css that gets generated when running yarn build.

Testing it all out

If you're following along, your package now supports a .scss file that pulls in design tokens, and exports a .main.css file within a dist folder.

To test this, you can create a boilerplate app like Create React App where you install:

yard add @my-awesome-blog-post/fonts --dev

In index.js you can then import it:

import '@my-awesome-blog-post/fonts'

Which will then let you reference the css variable in App.css:

* {
  font-family: var(--font-family);
}

And if everything has gone well, when you run yarn start, you should see the boilerplate app use your custom font. Hooray!

For design systems

To ensure every component receives the right font, I like to add a mixin that defines the font-family.

/* mixin */
@define-mixin $font-fonty {
  font-family: var(--font-family);
  font-style: normal;
}

/* button.css */

button {
  @mixin $font-fonty...;
}

Wrapping up

"Just set up a place to centralize fonts!" was a bigger ask than I had initially anticipated, but it was super rewarding to work on this project from start to finish.

A big thank you to @RickyRomero who helped me navigate every font-related hiccup and @EssPow who entertained my ramblings and pushed for a more elegant solution.