Skip to content

An Intro to Design Tokens, Style Dictionary, and Runtime Theming

The traditional mechanism for compiling MFEs has been for each MFE to pull in the common Paragon SCSS code from @edx/paragon, and the command branding SCSS code from @edx/brand into the MFE itself and then compile it together to produce the CSS files for the MFE.

There are some obvious flaws in this approach, the big ones being:

  • Each MFE will compile the same core Paragon theme code, leading to wasted time
  • Each MFE will include an independent copy of the Paragon theme code, leading to duplicated space
  • For each MFE a user will have to download mostly similar CSS code, leading to larger slower downloads
  • For any small change to the theme, each MFE will have to be compiled again, again leading to slower builds and deployments

There is now a new approach being worked on in the platform which will eliminate these issues. In this new system, you have a core set of Paragon CSS files that can be served by a CDN and consumed by all MFEs. These are reused across all MFEs so if a user visits one, they will have a cached copy and other MFEs will load faster. On top of this core set each MFE can include its own styling in a separate sheet.

This wasn't possible earlier since each MFE needed to load the Paragon theme to be able to load SCSS variables and use them in the MFE. However, if Paragon supports CSS variables, then MFEs can simply reference variables that are defined in another CSS file without needing to have it available at build time.

On top of this, this new system includes a mechanism using Design Tokens provided using "Style Dictionary" to build a CSS variable-based theme on top of the existing base Paragon theme. That's a lot of jargon for, you can use a new system to build a CSS file that has all the relevant CSS variables needed to customise the branding of a site. The location of this CSS file can be supplied via the MFE Config API, so you can have each site or MFE have a different theme.

We've now introduced a couple of new terms, so let's go over those briefly.

  1. CSS Custom Properties or CSS Variables
  2. Design Tokens
  3. Style Dictionary

CSS Custom Properties

One of the major features SCSS had over CSS was the ability to create variables that can be reused in multiple places. Instead of specifying a colour like #851C54 in a hundred different places, you can specify $my-color: #851C54 and then use $my-color wherever you wanted.

Now CSS has a way of doing this using CSS custom properties. This relatively new feature allows you to specify values that can be reused across your stylesheets. The same example as before using pure CSS would be:

1
2
3
4
5
6
7
:root {
    --my-colour: #851C54;
}

.my-div {
    background-color: var(--my-colour);
}

The advantage here is that if you separate out all your definitions of CSS variables into a separate stylesheet, you can retain most of your CSS and radically modify your site by simply swapping out the variable definitions.

With the new Paragon system, every aspect of Paragon that can be customised has a CSS variable you can set to override it. For instance, you have: --pgn-color-btn-disabled-bg-inverse-outline-primary. As you might be able to tell, it is the background colour to use for a button that is disabled and has the outline-primary type.

If you want, you can hand-craft a theme by just providing all the variables like able that need to be overridden. However Paragon also provides tools to simplify this using design tokens provided in the form of style dictionaries.

Design Tokens

Design tokens is just a fancy way of saying "variables related to UX design". Its design decisions translated to data so that it can be consumed or translated in multiple ways.

For instance, if you want to represent the background colour of the header of a pop-over, it could be represented by the token color.popover.header.bg. This token could be given a value of #851C54 and then this value can be used when and where needed.

What you need is some standard way to organise these tokens, supply their value and then convert it into the form you need. For example, you could use the above token in a website if it's converted to a CSS variable like --color-popover-header-bg. It could even be rendered to SCSS for another application, or rendered to a form that Android or iOS apps understand, so they can be themed as well.

Essentially, design tokens are meant to be a source of truth and then rendered into other formats as needed. Paragon uses a system called Style Dictionary to specify these variables and render them to CSS variables.

Style Dictionary

Design tokens are a concept that can be implemented in many ways using many formats. Style Dictionary is a web-focussed implementation that primarily uses JSON files to represent the tokens in a hierarchical manner.

Style Dictionary needs to be given a folder filled with JSON files that have token values, and it will merge them all and produce an output in multiple form. Paragon, however, only focusses on CSS for now, so the tokens are compiled into CSS variables.

Taking the previous example, if you wanted to specify the value for the color.popover.header.bg token, you'd specify it as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "color": {
        "popover": {
            "header": {
                "bg": {
                    "value": "#851C54"
                }
            }
        }
    }
}

This might seem a bit verbose, but the power of this system really kicks in when you consider that you can reference other variables. For instance, if you wanted to say that, the above colour should be the same as the background colour of a secondary button, you could set the value to "{color.btn.bg.secondary}" and the style dictionary system will automatically fills in the value. It can also transform values, such as darkening or lightening colours.

The output produced will be CSS files with the variable values from the tokens. So the above colour would be rendered as --pgn-color-popover-header-bg: #851C54;. These CSS files can then be uploaded to a CDN and specified in site or tenant config, so they will be provided via the MFE config API.

MFE Config for themes

Once you have a theme built, you can load it in an MFE by providing its URL in site or tenant configuration. The MFE will then fetch these values from the MFE Config API, and dynamically load them at runtime. This has a few benefits:

  • The theme can now be varied along the same axis as the MFE config API, i.e. on a per-site, and per-mfe level. So you can technically have a different theme for each MFE and site.
  • Changes to the theme can be deployed without needing to redeploy an entire MFE. This can be done by either changing the URL in the config, or by changing the contents of the CDN.

The MFE config API needs to have the following structure for the URLs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
    "PARAGON_THEME_URLS": {
        "core": {
            "urls": {
                "default": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css",
                "brandOverride": "<CDN>/core.css"
            }
        },
        "variants": {
            "light": {
                "urls": {
                    "default": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css",
                    "brandOverride": "<CDN>/theme-name.css"
                },
                "default": true,
                "dark": false
            }
        }
    }
}

In the above config, with core.urls.default we are providing a link to the upstream Paragon core theme which has the bulk of the CSS code. If our theme has an override for this, you can specify that using core.urls.brandOverride. After that you have variants.light.urls.default which has the default upstream version of the light theme variant. The idea is to eventually have a dark variant as well and potentially load the correct one based on preferences. Currently, only the light variant is supported.

The $paragonVersion above is special sytax, so the correct version of Paragon will automatically be filled in there so if different MFEs are using slightly differnt versions, that's OK.

In siple-theme we now have a CI setup so each merged PR that modifyes the theme will automatically build the CSS and push it to an S3-compatible storage. This means updating a theme can be as simple as making a PR and mergning it. Rollback can be as simple as reverting a PR and mergning.