Skip to main content

Tip: Don’t Preprocess What You Can Design Token

By Tyler Sticka

Published on June 28th, 2021

Topics

CSS preprocessors like Sass, Less and PostCSS extend the syntax of our stylesheets with extra functionality that can make development a lot more convenient. Let’s say your project includes a modular scale to maintain proportions responsively across typography and spacing. Instead of doing all the maths (or relying on a calculator) for every rule, we can write a function that accepts the desired step in our scale…

@use 'sass:math';

@function step($step, $base: 1em, $ratio: 1.5) {
  @return math.pow($ratio, $step) * $base;
}
Code language: SCSS (scss)

…and returns the correct value:

@use 'path/to/functions/mod';

h1 {
  font-size: mod.step(3); // => 3.375em
}

h2 {
  font-size: mod.step(2); // => 2.25em
}

h3 {
  font-size: mod.step(1); // => 1.5em
}
Code language: SCSS (scss)

Problem solved, right?

But let’s say you want those heading sizes to be consumable by more than just your stylesheets: Maybe you want to integrate them into the WordPress block editor, document them dynamically in Storybook, use them in a native app, or something else entirely.

What we need are design tokens! But in that case, we have some new problems. mod.step means nothing outside the context of our Sass files. And if we only tokenize the scale steps, we’ll have to rewrite the transformation math for every platform we support. (Yuck!)

So let’s move that logic from our Sass to our tokens, starting with that modular scale function…

const modStep = (step, ratio = 1.5) => {
  return ratio ** Number(step);
}

// Convenience method for em units
const modStepEm = (...args) => {
  return `${modStep(...args)}em`;
}

exports.modStep = modStep;
exports.modStepEm = modStepEm;
Code language: JavaScript (javascript)

…which we can bake into our token definitions. In Style Dictionary, we can use the same techniques as any Node.js project:

const { modStepEm } = require('../path/to/modular-scale.js');

exports = {
  size: {
    font: {
      heading_1: { value: modStepEm(3) },
      heading_2: { value: modStepEm(2) },
      heading_3: { value: modStepEm(1) },
    }
  }
};
Code language: JavaScript (javascript)

Or you could use a custom transform instead, which may be preferable if you want to store the steps as separate tokens or convert the units differently for different platforms. (This works in Theo, too.)

Regardless of how exactly you pull this off, the results should be the same. We can reference our token values in Sass…

@use 'path/to/tokens';

h1 {
  font-size: tokens.$size-font-heading-1; // => 3.375em
}

h2 {
  font-size: tokens.$size-font-heading-2; // => 2.25em
}

h3 {
  font-size: tokens.$size-font-heading-3; // => 1.5em
}
Code language: SCSS (scss)

…or JavaScript…

import { size } from './path/to/tokens.js';

console.log(size.font.heading_1.value); // => '3.375em'
console.log(size.font.heading_2.value); // => '2.25em'
console.log(size.font.heading_3.value); // => '1.5em'
Code language: JavaScript (javascript)

…or PHP…

$tokens_json = file_get_contents('path/to/tokens.json');
$tokens = json_decode($tokens_json);

echo $tokens->size->font->heading_1; // => '3.375em'
echo $tokens->size->font->heading_2; // => '2.25em'
echo $tokens->size->font->heading_3; // => '1.5em'
Code language: PHP (php)

…and so on!

By packaging our design token values and logic together instead of confining them to our CSS preprocessor, we make it much easier to reuse those values across different platforms. Something to consider the next time you reach for @function or @mixin!