Tip: Don’t Preprocess What You Can Design Token

Written by Tyler Sticka on

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;
}

…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
}

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;

…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) },
    }
  }
};

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
}

…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'

…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'

…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!

Tyler Sticka

Tyler Sticka is Cloud Four’s VP of Design, allowing him to think about design systems every day. When he isn’t directing his team, sketching on sticky notes or nitpicking CSS, he enjoys reading comics, making video games and listening to weird music. He tweets as @tylersticka.

Never miss an article!

Get Weekly Digests


Leave a Comment

Please be kind, courteous and constructive. You may use simple HTML or Markdown in your comments. All fields are required.


Let’s discuss your project! Email Us