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

Create a sd.config.js or sd.config.json, which will hold your Style Dictionary configuration:

JSON:

{
  "source": ["tokens/**/*.json"],
  "platforms": {
    "css": {
      "transformGroup": "css",
      "prefix": "sd",
      "buildPath": "build/css/",
      "files": [
        {
          "destination": "_variables.css",
          "format": "css/variables"
        }
      ]
    }
  }
}

JS:

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

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

See Advanced Configuration for more explanation.

Token files

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

{
  "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" }
  }
}

The cool thing about putting your tokens in .js files however is that it opens you up to the possibility of easily reusing token partials.

Imagine that your Design System has multiple types of inputs, but they all share the same tokens for the label. Instead of duplicating those token definitions for each input, you can reuse:

input-tokens.js:

export default {
  label: {
    size: {
      value: '{font.base.value}',
    },
    color: {
      value: '{color.gray.900.value}',
    },
  },
};

input-amount.tokens.js:

import input from '../../shared/input-tokens.js';

export default {
  ...input,
  content: {
    border: {
      radius: {
        value: '{radii.md.value}',
      },
      color: {
        value: '{color.primary.500.value}',
      },
    },
  },
};

Note that the first file is .js and the second one is .tokens.js. This is on purpose!

We only want the latter one to be matched by the Style Dictionary source globs. If we match both, we might get some issues with duplicate tokens because we both match AND apply the reusable input token part.

You could also ensure this by putting them in different folders, up to you how you handle the matching.

Importing dependencies

Relative imports between 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.

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.

The import will be transformed and matched with the StyleDictionary object that Backlight uses under the hood. Therefore, you cannot set the version of this. No need to add it as a dependency either.

import StyleDictionary from 'style-dictionary';

const { formatHelpers } = StyleDictionary;

Advanced Configuration

When using .js for the config, it opens the doors for more advanced use cases for Style Dictionary.

Custom Format

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',
        },
      ],
    },
  },
};

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.

Real life example

If you want to see a working example in Backlight using most of these advanced features, check out out simba starter-kit .