Style Dictionary

Style Dictionary is a build system that allows you to define styles once, in a way for any platform or language to consume. A single place to create and edit your styles, and a single command exports these rules to all the places you need them - iOS, Android, CSS, JS, HTML, sketch files, style documentation, or anything you can think of. It is available as a CLI through npm, but can also be used like any normal node module if you want to extend its functionality.

We have built-in integration with this library in Backlight, so that you can use Design Tokens in your format of choice as a single source of truth, and export to the platforms that you need, e.g. CSS custom properties, JS variables, etc.

If you want to play a bit with Style Dictionary on its own before using it in your Design System in Backlight, we built an interactive playground just for this that you can use!

Usage

You will need two things:

  • Configuration file
  • Token files

Whenever you change a token file (as matched by the array of globs in your configuration) or your configuration file, Style Dictionary will automatically run under the hood of Backlight. You can see output logs in the console.

Configuration

There's a few options that we support for your style-dictionary configuration file.

The file must be in the root of the project and the name must match: sd.config.{json,js,cjs,mjs,ts}. This is currently not configurable.

In this example, let's start by creating a sd.config.js which will hold your Style Dictionary configuration:

sd.config.js:

module.exports = {
  source: ['**/*.tokens.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      prefix: 'sd',
      buildPath: 'tokens/',
      files: [
        {
          destination: '_variables.css',
          format: 'css/variables',
        },
      ],
    },
  },
};

If you use a .js file for your config, it will allow for more flexibility, for example adding your own custom parsers, transforms and formats.

See Custom Formats for more explanation.

Token files

Token files can be either JSON or JS files as long as they match your source array in your Style Dictionary config.

Theoretically, you can write your token files in any language, as long as you can write a parser for it.

radii/src/radii.tokens.json:

{
  "radii": {
    "none": { "value": "0" },
    "sm": { "value": "0.125rem" },
    "base": { "value": "0.25rem" },
    "md": { "value": "0.375rem" },
    "lg": { "value": "0.5rem" },
    "xl": { "value": "0.75rem" },
    "xl2": { "value": "1rem" },
    "xl3": { "value": "1.5rem" },
    "full": { "value": "9999px" }
  }
}

After creating the config and this token file, you should get a tokens/_variables.css file containing 9 CSS custom properties, representing the tokens.

Running locally

In order to run style-dictionary locally, rather than using the Backlight integration, you can run from the root of the project:

npx style-dictionary build --config sd.config.js

Note that for local style-dictionary, your config must be CommonJS or JSON format, it's only in Backlight that you have ESM integration working out of the box.

Real life example

If you want to see a working example in Backlight using most of these advanced features, check out our rev design system.

Want to learn how to export your Design Tokens to Figma? Read this guide.

Advanced use cases

Now that you understand the basics, here are some more advanced use cases.

Custom Formats

Below we create a custom format for something called CSS Literals which is how Lit tends to do CSS-in-JS styles.

const tokenFilter = (cat) => (token) => token.attributes.category === cat;

export default {
  source: ['**/*.tokens.js'],
  format: {
    cssLiterals: (opts) => {
      const { dictionary, file } = opts;
      let output = formatHelpers.fileHeader(file);
      output += `import { css } from 'lit';\n\n`;

      dictionary.allTokens.forEach((token) => {
        const { path, original } = token;

        // Use the path of the token to create the variable name, skip the first item
        const [, ..._path] = path;
        const name = _path.reduce((acc, str, index) => {
          // converts to camelCase
          const _str =
            index === 0 ? str : str.charAt(0).toUpperCase() + str.slice(1);
          return acc.concat(_str);
        }, '');

        output += `export const ${name} = css\`${original.value}\`;\n`;
      });

      return output;
    },
  },
  platforms: {
    js: {
      transformGroup: 'js',
      buildPath: '/',

      // Could be abstracted further e.g. function that accepts array
      // of categories and generates these objects
      files: [
        {
          filter: tokenFilter('colors'),
          destination: 'colors/src/_colors.js',
          format: 'cssLiterals',
        },
        {
          filter: tokenFilter('spacing'),
          destination: 'spacing/src/_spacing.js',
          format: 'cssLiterals',
        },
        {
          filter: tokenFilter('typography'),
          destination: 'typography/src/_typography.js',
          format: 'cssLiterals',
        },
        {
          filter: tokenFilter('radii'),
          destination: 'radii/src/_radii.js',
          format: 'cssLiterals',
        },
      ],
    },
  },
};

JSON5

JSON5 is supported in style-dictionary, because it mutates the JSON object in NodeJS through a global register.

Right now this feature is not yet enabled in Backlight, but we are planning to support it in the future.

Style Dictionary Object

You can import the StyleDictionary object, which is useful if you need to do more advanced stuff or use its formatHelpers.

In Backlight, the import will be transformed and matched with the StyleDictionary object that Backlight uses under the hood.

import StyleDictionary from 'style-dictionary';

const { formatHelpers } = StyleDictionary;

Single-token Format Wrapper

Here's a cool blogpost by one of the maintainers of Style Dictionary.

It's about different ways to approach dark mode with Style Dictionary. Highlighting one example approach is the Single-token method.

Snippets below taken from the blogpost, although with small adjustments

You can write dark mode variants into the same token as the light variant, and apply a format wrapper that mutates the dictionary to use the darkValue when building for dark mode.

// tokens/color/background.json5
{
  "color": {
    "background": {
      "primary": {
        "value": "{color.core.neutral.0.value}",
        "darkValue": "{color.core.neutral.1000.value}"
      }
    }
  }
}

Then create a wrapper for your formats that mutate the dictionary to use the darkValue, right before applying the format.

import StyleDictionary from 'style-dictionary';

function darkFormatWrapper(format) {
  return function (args) {
    // Create a local copy
    const dictionary = { ...args.dictionary };

    // Override each token's `value` with `darkValue`
    dictionary.allTokens = dictionary.allTokens.map((token) => {
      const { darkValue } = token;
      if (darkValue) {
        return {
          ...token,
          value: token.darkValue,
        };
      } else {
        return token;
      }
    });

    // Use the built-in format but with our customized dictionary object
    // so it will output the darkValue instead of the value
    return StyleDictionary.format[format]({ ...args, dictionary });
  };
}

export default {
  // add custom formats
  format: {
    cssDark: darkFormatWrapper(`css/variables`),
  },
  //...
  platforms: {
    css: {
      transformGroup: `css`,
      buildPath: '/',
      files: [
        {
          destination: `variables.css`,
          format: `css/variables`,
          options: {
            outputReferences: true,
          },
        },
        {
          destination: `variables-dark.css`,
          format: `cssDark`,
          filter: (token) =>
            token.darkValue && token.attributes.category === `color`,
        },
      ],
    },
  },
};

It's important that you use allTokens and NOT the old allProperties as shown in the blogpost.

Importing dependencies

Relative imports between JS token files and the configuration files work straight away.

Bare imports to third party dependencies can work too but have some caveats.

Let's take a library like tinycolor2 which may be used in a token file to transform between rgb and hex.

import tinyColor from 'tinycolor2';

This does not work at the time of writing this, but can be rewritten to load from our CDN:

import { __moduleExports as tinyColor } from 'https://srv.divriots.com/packd/tinycolor2@1.4.2?flat';

Or if that module only has 1 default export, ?flat will not return anything.

In that case you can do:

import { packd_export_0 as mod } from 'https://srv.divriots.com/packd/tinycolor2@1.4.2';
const { default: tinyColor } = mod;

We are working on improving this flow.