Switch to šŸŒ‘ mode

Exposing Figma Tokens to Design System consumers

February 14, 2023

Hello! Today Iā€™ll share my experience developing and implementing design system components using Tokens Studio (formerly Figma Tokens) and I will tell you how we expose those tokens to DS consumers so they can use them as well.

Having been a developer at Rangle for 3 years, Iā€™ve worked on many different projects. Almost all of them involved Design Systems. If you want to know more about Design Systems, take a look at this page where my colleagues have gathered all the information you might need about DS.

A design system could be based on a third-party library or developed internally. It could contain only a few elements or be a complex system of hundreds of different components. Nowadays, you have many options which give you the functionality you are looking for out of the box. Well thought out and created components will expose API which covers all most common cases. And the adoption of the third-party library is mostly about ā€œmaking exposed elements look as per designā€. The same applies to self-developed DS components, you still need to style them.

You might think that styling is not the most sophisticated part of DS adoption. These days, many design tools can generate CSS, so you as a developer can just grab and paste them where appropriate. But what if a project needs theming? Or letā€™s think about a situation when you have already implemented a bunch of different components and then some design change decision was made (for example change of the secondary color) and you need to go through each of these elements and manually update them. This is not optimal. Itā€™s a time-consuming process and you might just miss some components.

Design tokens

How can you avoid situations when the implementation of the design and especially updating existing components with new design becomes a nightmare? The answer is: you need to use design tokens!

Design tokens are a set of design system variables that can be used to ensure consistency across different design elements, such as colors, typography, and spacing.

While working on our design system, we used Figma ā€“ a collaborative web application for interface design ā€“ as a source of truth for our design and we used a special plugin to generate and export tokens. Tokens Studio is a plugin that allows users to create and manage design tokens within Figma projects, making it easier to maintain consistency and make updates to the design system. The plugin also allows for the export of tokens to various formats including JSON, SCSS, and CSS Custom Properties.

We used this plugin to organize work around our tokens so we can easily use it for both: design and development matters.

This way we make our interactions within the team really smooth and simple. We all knew what particular token should be used just by looking at Figma.

But before we can use tokens inside our codebase, we need somehow to export them from Figma to our codebase.

Export tokens

As I mentioned before, the Tokens Studio plugin gives you the ability to export all your tokens.

In our case, among basic (first-tier) tokens, we are using semantic (second-tier) tokens as well. That's why we decided to export all the tokens as JSON object. That way, we can parse this object and apply basic tokens to all the references (semantic tokens) as we want to create js tokens that can be used inside our code for both of them.

The Tokens Studio plugin can use GitHub as token storage. To make it possible, you need to set it up with GitHub PAT, also you need to specify a repository and a branch where to store your JSON object.

That way, designers can easily send changes to the GitHub repo. What you need to do as a developer though is to ensure that everything works as expected and that changes designers introduced will not break the existing codebase. To avoid that happening, we created a GitHub Actions job -you can read more about how to use GitHub Actions in my previous post - which will be triggered each time designers will create a PR to the dedicated branch we mentioned earlier.

Generate JS tokens

Cool! We have a JSON object in our code. Here you have an example that includes tokens of certain types (core-spacing) as an object containing different options (0 and 100) which in turn is described as an object with value and type parameters.

{
  "core-spacing": {
    "0": {
      "value": "0",
      "type": "spacing"
    },
    "100": {
      "value": "2px",
      "type": "spacing"
    }
  }
}

Now, we need to convert them to JS tokens so we can use them as variables inside our components.

We are using CSS-in-JS approach to style all the elements inside our design system, but you can also use pure CSS or SCSS.

Before we get to the code, I need to explain one important thing. Starting now and down through the text when we speak about basic tokens I will be meaning a structure we defined as a basic token, in our case it is an object that has value and type parameters.

"base": {
    "value": "#7a00e6",
    "type": "color"
  },

Here you see an example of basic token called base with a value of #7a00e6 and type of color.

To be able to determine whether a token is basic or not we introduced isBasicToken function. It checks if the object contains a type parameter.

export const isBasicToken = (
  token: [string, FigmaTokenProperties | RawFigmaTokenObject]
): token is [string, FigmaTokenProperties] =>
  Object.prototype.hasOwnProperty.call(token[1], "type")

To generate our JS tokens we created a bunch of functions, letā€™s take a look deeply on what we did. generateJSFile function is the main function that will be triggered each time we want to parse a JSON object.

export const generateJSFile = async (tokens: RawFigmaTokenObject) => {
  const parsedTokens = parseFigmaTokens(tokens)

  let fileContent = `// Generated on ${new Date().toLocaleString()}\n`
  fileContent += `export * from '../staticTokens/types';\n`
  parsedTokens.forEach((value, key) => {
    fileContent += `export const ${key} = ${JSON.stringify(
      value,
      null,
      2
    )} as const;\n`
  })

  await fs.writeFileSync("./generated/theme.ts", fileContent)

  parsedTokens.forEach(renderStory)
}

It accepts a tokens parameter which represents a JSON object containing all the exported Figma tokens. After we get parsedTokens this function will create a TS file where we write all the tokens. We will come back later to the actual values. This generated file also includes an import of static types (in our case we defined some additional types and constants to facilitate the usage of certain variables).

Now letā€™s jump to parseFigmaTokens function:

export const parseFigmaTokens = (tokens: RawFigmaTokenObject) => {
  const schemaObject: SchemaMap = new Map()
  const colorMap: NestedTokensMap = new Map()
  const typographyMap: NestedTokensMap = new Map()

  Object.entries(tokens).forEach(([key, item]) => {
    const camelCasedKey = getCamelCasedKey(key)
    schemaObject.set(camelCasedKey, {})
    Object.entries(item).forEach(token => {
      flattenValues(camelCasedKey, token, schemaObject, colorMap, typographyMap)
    })
  })
  return schemaObject
}

Here you might notice that we created a couple of Map objects (yes, javascript has it). schemaObject is our main object where we store all the basic tokens. colorMap and typographyMap represent objects containing only basic tokens of a certain type. We need them so we actually can parse semantic tokens we received from Figma.

The next lines of code are very specific to the way we expose tokens from Figma. And what this code is doing is getting specific information we need and passing it to flattenValues function.

export const flattenValues = (
  section: string,
  tokenEntry: [string, RawFigmaTokenObject | RawFigmaTokenProperties],
  schemaObject: SchemaMap,
  colorMap: NestedTokensMap,
  typographyMap: NestedTokensMap,
  tokensGroup: string[] = []
) => {
  if (isBasicToken(tokenEntry)) {
    setTokensMap(
      section,
      tokenEntry,
      schemaObject,
      colorMap,
      typographyMap,
      tokensGroup
    )
  } else {
    Object.entries(tokenEntry[1]).forEach(innerToken => {
      flattenValues(
        section,
        innerToken,
        schemaObject,
        colorMap,
        typographyMap,
        [tokenEntry[0], ...tokensGroup]
      )
    })
  }
}

Here we need to do a recursion because it could be that basic tokens are nested more than 1 level down, and we donā€™t know exactly how deeply they are nested. When isBasicToken returns true our code will fall into an if condition where setTokensMap will be called.

export const setTokensMap = (
  section: string,
  [basicTokenName, basicTokenProperties]: [string, FigmaTokenProperties],
  map: SchemaMap,
  colorMap: NestedTokensMap,
  typographyMap: NestedTokensMap,
  tokensGroup: string[]
) => {
  const sectionObject = map.get(section) || {};
  const groupToken = tokensGroup[0];

  switch (basicTokenProperties.type) {
    case 'boxShadow';
    case 'typography';
    case 'color';
    case 'fontFamilies';

    default:
    map.set(section, {
      ...sectionObject,
      [basicTokenName]: basicTokenProperties.value,
    });
  }
};

This function is responsible for writing values to the schemaObject map. If we take a look at the example I shared before, what this function does is group all the tokens by sections (types) and assign the value to a particular token (option). We also have a couple of cases defined in the code. They cover some edge cases where we need additional manipulations. For example, we have semantic tokens exported from Figma. These tokens are references to values of basic tokens, which is why we need colorMap to be filled with basic tokens first. And again this is very related to how we received tokens from Figma.

After parseFigmaToken will be executed we will get a file containing such a structure:

export const coreSpacing = {
  "0": "0",
  "100": "2px",
} as const

export const colorCore = {
  brand: {
    base: "#7a00e6",
  },
} as const

These tokens can be used as coreSpacing[100] or colorCore.brand.base.

Exposing JS tokens to DS consumers

Tokens we got already can be used inside our project. Thanks to the power of typescript, our tokens already support autocomplete and have correct ts types.

But we decided to take one step forward and make it possible for DS consumers to use these tokens independently. In chasing this idea we created a separate npm package containing all the tokens and assets (fonts, breakpoints, etc.).

We made this package to be compatible with tools such as Jest or Chromatic. In doing so we include both CommonJS and ESModules exports to our package. So users donā€™t need to perform any additional setup to their environment before using our package.

This way we support our design system with powerful and easy-to-use JS tokens that DS consumers can use to create custom components or adapt the brand-looking design in a smooth and efficient manner.


Profile picture

Written by Pavel Ivanov in sunny Spain. Reach me on LinkedIn. Or just take a look at my CV.

* use of site content is strictly prohibited without prior written approval