Theo Design Tokens Using Node-Sass Importer For Any Build Method

Design Tokens are a wonderful way to codify all the small decisions in a Design System and help ensure consistency across the code base. Often these take the form of simple variables in our Sass stylesheets, however that limits their use to only that code language; and that hamstrings the potential benefit of their ubiquitous use. We want to use them in many more places: JavaScript, templating languages, style guides, Sketch, and APIs.

In order to do that, we need them in a universal format: JSON. We'll use the tool Theo from Salesforce which converts Design Token files written in JSON or Yaml to many different formats: scss, css custom properties, common.js, ES modules, and more. Here's an example of what Design Tokens written in this format look like:
The file aliases.yml:

    aliases:
      primary_color:
        value: "#0070d2"

The file tokens.yml:

    props:
      button_background:
        value: "{!primary_color}"
        type: color
        category: buttons
        comment: This should be used for main buttons
    imports:
      - ./aliases.yml

If you told Theo to use the scss format, it would make a tokens.scss file like so:

    // This should be used for main buttons
    $button_background: #0070d2;

A few great features I want to call out:

  • The ability to have a variables value be another variable (what they call "aliases" and are used with the format {!primary_color}). This can't be done by normal JSON/Yaml, and is very important for design tokens: it's considered best practice to have a variable for "blue", "green", etc and then set your "primary" color variable to the "blue" variable. Then throughout your system, you use "primary" instead of "blue" - this makes it very easy when a redesign of the brand happens or when components are use by multiple brands, each with their own "primary" color.
  • The ability to break your file up via imports
  • Comments and categories for basic documenting.

So the problem that we came across is that in order to compile our code we had to run Theo to turn Yaml files into a scss file of variables before we compiled the rest of our scss files (which contained @import "tokens.scss"; statements that pulled in the file Theo made. There's a number of problems with this approach I won't go deep into, but build pipelines like this are best kept in-memory as opposed to multiple file system read & write operations.

I wanted our team to be able to import the Design Token Yaml files directly with @import "tokens.yml"; in our Scss files. Fortunately, the ability to add custom importers right to node-sassexists, which is used by pretty much every node.js based sass compiler: gulp-sass, sass-loader for WebPack, postcss-scss for PostCSS, and of course: just using the CLI provided by node-sass itself. In short: this will work everywhere you use Sass; unless of course you're still using the Ruby gem (which has long since dwindled in popularity).

Setup

Alright, let's get started. The [node-sass docs for the options it takes shows how we can pass in importer](https://www.npmjs.com/package/node-sass#importer--v200---experimental), these options get passed in a slightly different way depending on what you're using (Webpack, Gulp, etc). In all cases, it says that importer is an array of functions that get called every time sass comes across an @import "something"; and then calls those functions with the file path that got imported and the file path it got imported from. Then you either return the absolute path to the file or the file contents as a string. So when a .yaml or .yml file is imported, we'll convert it using Theo and then return the scss variables.

Go ahead and install Theo:

    npm install --save theo

Then make a new JS file (like theo-importer.js) with the below contents - there are lots of comments and annotations to guide you through it!

const theo = require('theo'); // <https://www.npmjs.com/package/theo>
    const { resolve, parse } = require('path');

    /**
     * Theo Design Token Sass Importer
     * Import scss variables from Yaml files directly
     * @param {string} url - path to the file passed into import statement, i.e. `@import "design-tokens.yml";`
     * @param {string} prev - path to the file the import statement is located at, useful for calculating relative paths
     * @param {function({ file: string, contents: string })} done - callback to fire when done; pass in `file` for path to scss file to import OR `contents` with the contents of a scss file
     * @link <https://www.npmjs.com/package/node-sass#importer--v200---experimental>
     * @link <https://www.npmjs.com/package/theo>
     */
    function theoImporter(url, prev, done) {
      // If the imported file doesn't end in `.yml` or `.yaml`, then `return null` early to tell node-sass that we're not going to do anything. It'll go on to the next function or just try to handle the import itself.
      if (!/\\.ya?ml$/.test(url)) return null;

      // `prev` is the where it was imported from, we just want the directory it is in
      const prevDirectory = parse(prev).dir;
      // imports are almost always relative, so let's figure out how to get to there from here so we end up with an absolute url
      const designTokenFilePath = resolve(prevDirectory, url);
      theo
        .convert({
          transform: {
            type: 'web',
            file: designTokenFilePath,
          },
          format: {
            // This can be any format Theo supports (or your own custom one!) <https://www.npmjs.com/package/theo#formats>
            // We're choosing scss variables with `!default`
            type: 'default.scss',
          },
        })
        .then(scssString => {
          // `scssString` will be our scss variables, and instead of writing to a file, we'll just pass it back to Sass
          done({
            contents: scssString,
          });
        })
        .catch(({ message }) => {
          const msg = `Theo design token error in ${url} - ${message}`;
          done(new Error(msg));
        });
    }

    module.exports = theoImporter;
## Implementation

Here's how this would get used in a few different methods:

Using gulp-sass

const theoImporter = require('./theo-importer');
    gulp.task('sass', function() {
      return gulp
        .src('./sass/**/*.scss')
        .pipe(
          sass({
            importer: [
              theoImporter,
            ],
          }).on('error', sass.logError),
        )
        .pipe(gulp.dest('./css'));
    });

Using Webpack's sass-loader

const theoImporter = require('./theo-importer');

    // webpack.config.js
    module.exports = {
      modules: {
        rules: [
          {
            loader: 'sass-loader',
            options: {
              importer: [theoImporter],
            },
          },
        ],
      },
    };

Using the node-sass CLI

    node-sass --importer ./theo-importer.js src/style.scss dist/style.css

Usage

You're all set! Then any of your Sass files:

    @import "tokens.yml";

    button {
      background: $button_background;
    }

I hope this helps out! Getting your Design Tokens in a universal format that any programming language or application can understand really helps them fulfill their promise of bringing consistency and clarity to the wide array of brand experiences by not tying them to a specific programming language like Sass, nor a platform like the web. Brands are so much bigger than a single website or a single method of interaction via a web browser; given the multitude of mediums they are designed for, bringing consistency promotes professionalism, trust, and ease of recognition. These are hard challenges to accomplish and it starts with the smallest building block: Design Tokens.