DEV Community

Cover image for Converting Design Tokens to CSS Variables using Style Dictionary
Anna Popova
Anna Popova

Posted on • Updated on

Converting Design Tokens to CSS Variables using Style Dictionary

Setting up a Next.js project

As a starting point for building a test project, we used a Next.js Typescript template. This boilerplate implements core principles of JAMstack and allows to quickly create serverless applications.

To set up the project environment, it is necessary to install the following dependencies:

// package.json

dependencies: {
  "style-dictionary": "",
  "token-transformer": "",
  "tailwindcss-box-shadow": "", // opt
  "tinycolor2": "", // opt
}
Enter fullscreen mode Exit fullscreen mode

Additionally, don't forget to regularly update your dependencies to ensure that token modifications are processed correctly.

The project has a standard file structure. The design tokens are placed within the styles directory. Additionally, the build.js file, which runs Style Dictionary, is located in the scripts folder.

├── styles
│   ├── scripts
│   │   ├── build.js
│   │   └── fns.js
│   ├── tokens
│   │   ├── input
│   │   │   ├── base
│   │   │   │   └── global.json
│   │   │   └── themes
│   │   │       ├── dark
│   │   │       │   └── dark.json
│   │   │       └── light
│   │   │           └── light.json
│   │   └── output
│   │       ├── dark.json
│   │       ├── global.json
│   │       └── light.json
│   ├── dark.css
│   ├── global.css
│   ├── index.css
│   └── light.css
Enter fullscreen mode Exit fullscreen mode

Automating Design Token updates

The Figma project is synchronized with the repository, and when the designer pushes any changes, that triggers a pipeline, which transforms raw data into CSS variables.

Workflow of transferring design tokens from Figma to JSON, processed through Style Dictionary and integrated into Tailwind CSS

Design Token files must go through several transformation steps to return valid style sheets. The token-transformer utility replaces the references with calculated values so that the JSON object conforms to the Style Dictionary standards. The --expandTypography option can be used to convert every font-related property into an individual object.

// package.json

"scripts": {
  "transform-tokens-dark": "npx token-transformer app/ui/styles/tokens/input/themes/dark app/ui/styles/tokens/output/dark.json",
  "transform-tokens-global": "npx token-transformer app/ui/styles/tokens/input/base app/ui/styles/tokens/output/global.json --expandTypography",
 "transform-tokens-light": "npx token-transformer app/ui/styles/tokens/input/themes/light app/ui/styles/tokens/output/light.json",
  "transform-tokens": "yarn transform-tokens-light && yarn transform-tokens-dark && yarn transform-tokens-global",
  "tokens": "node app/ui/styles/scripts/build.js"
}
Enter fullscreen mode Exit fullscreen mode

Commands in package.json support design token workflow:

// Transform design tokens
yarn transform-tokens 

// Build and update CSS variables
yarn tokens 
Enter fullscreen mode Exit fullscreen mode

Global style settings

In global.json file, we've established a foundational set of design variables that are essential across the entire project. This includes crucial aspects like typography, size and z-index.

Style Dictionary allows us to define functions and then to modify input values. For example, you can specify the letter-spacing property in ems.

// build.js

function transformLetterSpacing(value) {
  if (value.endsWith('%')) {
    const percentValue = value.slice(0, -1);
    return `${percentValue / 100}em`;
  }
  return value;
}

StyleDictionaryPackage.registerTransform({
  name: 'size/letterspacing',
  type: 'value',
  transitive: true,
  matcher: (token) => token.type === 'letterSpacing',
  transformer: (token) =>
  transformLetterSpacing(token.value)
});
Enter fullscreen mode Exit fullscreen mode

Theme-specific styles. Dark and Light mode definitions

In dark.json and light.json files, we focus on theme-specific styles, primarily defining colors and shadows tailored for dark and light modes.

The transform function can combine the number of parameters that define the appearance of a box-shadow into a CSS variable. The resulting value can be used in the theme configuration file after installing the tailwindcss-box-shadow plugin.

// tailwind.config.js

module.exports = {
  content: ['./app/**/*.{js,ts,jsx,tsx}'],
  darkMode: 'class',
  theme: {},
  plugins: [require('tailwindcss-box-shadow')]
}
Enter fullscreen mode Exit fullscreen mode
// build.js

StyleDictionaryPackage.registerTransform({
  name: "shadow/css",
  type: "value",
  transitive: true,
  matcher: (token) => token.type === "boxShadow",
  transformer: (token) => {
    const shadow = Array.isArray(token.value) ? token.value : [token.value];
    const value = shadow.map((s) => {
      const { x, y, blur, spread, color } = s;
      return `${x}px ${y}px ${blur}px ${spread}px ${tinycolor(color).toHslString()}`;
    });
    return value.join(", ");
  },
});
Enter fullscreen mode Exit fullscreen mode

Color Modifiers

Color Modifiers

Tokens Studio uses modifiers for fine-tuning color tokens, including lightening, darkening, mixing, and adjusting opacity. By creating transformers like color/hslAdjust in Style Dictionary, we can adapt tokens, darkening colors by a specified percentage and returning the result in HSL format. This approach allows for dynamic visual changes in interface elements, for example, darkening the hover token by 27% when the user hovers over it.

// light.json

"hover": {
  "value": "{color.blue.500}",
  "type": "color",
  "$extensions": {
  "studio.tokens": {
    "modify": {
      "type": "darken",
      "value": "0.27",
      "space": "hsl"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
// build.js

function resolveTokenValue(token, dictionary) {
  if (
    typeof token.value === 'string' &&
    token.value.startsWith('{') &&
    token.value.endsWith('}')
  ) {
    const resolvedToken = dictionary.getReferences(token.value);
    return resolvedToken ? resolvedToken.value : token.value;
  }
  return token.value;
}

function transformHSL(token, dictionary) {
  const resolvedValue = resolveTokenValue(token, dictionary);
  let color = tinycolor(resolvedValue);
  const modification = token.$extensions?.['studio.tokens']?.modify;
  if (modification && modification.type === 'darken') {
    color = color.darken(parseFloat(modification.value) * 100);
  }
  return color.toHslString();
}

StyleDictionaryPackage.registerTransform({
  name: 'color/hslDarken',
  type: 'value',
  matcher: (token) => token.type === 'color' && token.$extensions,
  transformer: (token, dictionary) => transformHSL(token, dictionary),
});
Enter fullscreen mode Exit fullscreen mode

Theme

Finally, during the building process, a set of CSS variables will be created. Style Dictionary will add selectors to style sheets as a combination of a :root pseudo-class and a themed class. These can be used later to swap from light to dark mode.

// build.js

files: [
  {
    destination: `${theme}.css`,
    format: 'css/variables',
    selector: `:root.${theme}`
  }
]
Enter fullscreen mode Exit fullscreen mode

The Tailwind configuration file stores the object that represents the current theme. And Design Tokens can be attached to component styles through CSS variables. Thus, we can update the external presentation of the User Interface by changing the values of the tailwind.config.js.

// tailwind.config.js

theme: {
  colors: {
    primary: {
      DEFAULT: 'var(--color-primary-default)'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To maintain consistency in UI design, we created a ThemeProvider component and placed it at the top-level of the React tree. That wrapper uses the Context API to pass the current theme data down to child components.

// App.tsx

export const App = () => (
  <ThemeContextWrapper>
    {/* children */}
  </ThemeContextWrapper>
);
Enter fullscreen mode Exit fullscreen mode

Oldest comments (0)