Skip to content

Sass Integration

Generate .scss and .sass from your Design Tokens Community Group (DTCG) tokens.

Setup

TIP

Make sure you have the Cobalt CLI installed!

Install the plugin (and its dependency) from npm:

sh
npm i -D @cobalt-ui/plugin-sass @cobalt-ui/plugin-css

Then add to your tokens.config.mjs file:

js
import pluginSass from "@cobalt-ui/plugin-sass"; 

/** @type {import("@cobalt-ui/core").Config} */
export default {
  tokens: "./tokens.json",
  outDir: "./tokens/",
  plugins: [pluginSass()], 
};

And run:

sh
npx co build

You’ll then generate a ./tokens/index.scss file that exports a token() function you can use to grab tokens:

scss
@use "../tokens" as *; // update "../tokens" to match your location of tokens/index.scss

.heading {
  color: token("color.blue");
  font-size: token("typography.size.xxl");
}

Usage

The generated Sass outputs the following helpers:

token()

The main way you’ll use the token is by importing the token() function to grab a token by its ID (separated by dots):

scss
@use "../tokens" as *; // update "../tokens" to match your location of tokens/index.scss

.heading {
  color: token("color.blue");
  font-size: token("typography.size.xxl");
}

body[color-mode="dark"] .heading {
  color: token("color.blue", "dark"); // pull "dark" mode variant
}

Note that a function has a few advantages over plain Sass variables:

  • ✅ The name perfectly matches your schema (no guessing!)
  • ✅ You can programmatically pull values (which is more difficult to do with Sass vars)
  • ✅ Use the same function to access modes

typography()

Sass mixin to inject all styles from a typography token. Optionally provide the mode as the 2nd param.

scss
@include typography($tokenID, [$mode]);
scss
@use "../tokens" as *;

h2 {
  @include typography("typography.heading-2");

  font-size: token("typography.size.xxl"); // overrides can still be applied after the mixin!
}

Note that you can override any individual property so long as it comes after the mixin.

listModes()

The listModes() function lists all modes a token has defined. This returns a Sass list. This can be used to generate styles for specific modes:

scss
@use "../tokens" as *;

@for $mode in listModes("color.blue") {
  [data-color-mode="#{$mode}"] {
    color: token("color.blue", $mode);
  }
}

CSS Variable Mode

By default, this plugin converts tokens to pure Sass variables. But if you’d like to take advantage of dynamic CSS variables (which support dynamic modes), you can use in conjunction with the CSS plugin. This gives you all the flexibility and benefits of modern CSS while still keeping the typechecking properties of Sass.

To use CSS variables instead of Sass variables, set the pluginCSS option (can be an empty object or contain configurations):

js
import pluginSass from "@cobalt-ui/plugin-sass";

/** @type {import("@cobalt-ui/core").Config} */
export default {
  plugins: [
    pluginSass({
      pluginCSS: {
        prefix: "ds",
        modeSelectors: [
          {mode: "light", tokens: ["color.*"], selectors: ['[data-color-theme="light"]']},
          {mode: "dark", tokens: ["color.*"], selectors: ['[data-color-theme="dark"]']},
        ],
      },
    }),

From here you can set any option the CSS plugin allows.

TIP

Don’t forget to import the ./tokens/tokens.css file into your app as well so those variables are defined!

WARNING

Don’t load another instance of @cobalt-ui/plugin-css, otherwise they may conflict!

Tips

Though CSS variable mode is recommended, there may be some caveats to be aware of. One example is that you’ll lose the ability for Sass to change opacity, however, you can achieve the same results with the color-mix() function:

scss
.text {
  color: rgba(token("color.ui.foreground"), 0.75); 
  color: color-mix(in oklab, #{token("color.ui.foreground")}, 25% transparent); 
}

You’ll also lose Sass’ ability to perform math on the values, however, CSS’ built-in calc() can do the same:

scss
.nav {
  margin-left: -0.5 * token("space.sm"); 
  margin-left: calc(-0.5 * #{token("space.ms")}); 
}

In either case, letting the browser do the work is better, especially considering CSS variables are dynamic and can be modified on-the-fly.

TIP

Always use in oklab as the default colorspace for color-mix(). It usually outperforms other blending methods (comparison).

Config

Here are all plugin options, along with their default values:

js
import pluginSass from "@cobalt-ui/plugin-sass";

/** @type {import("@cobalt-ui/core").Config} */
export default {
  tokens: "./tokens.json",
  outDir: "./tokens/",
  plugins: [
    pluginSass({
      /** set the filename inside outDir */
      filename: "./index.scss",
      /** output CSS vars generated by @cobalt-ui/plugin-css */
      pluginCSS: undefined,
      /** use indented syntax? (.sass format) */
      indentedSyntax: false,
      /** embed file tokens? */
      embedFiles: false,
      /** handle specific token types */
      transform(token, mode) {
        // Replace "sans-serif" with "Brand Sans" for font tokens
        if (token.$type === "fontFamily") {
          return token.$value.replace("sans-serif", "Brand Sans");
        }
      },
    }),
  ],
};

Color tokens

js
/** @type {import("@cobalt-ui/core").Config} */
export default {
  plugins: [
    pluginSass({
      colorFormat: "oklch",
    }),
  ],
};

By specifying a colorFormat, you can transform all your colors to any browser-supported colorspace. Any of the following colorspaces are accepted:

If you are unfamiliar with these colorspaces, the default hex value is best for most users (though you should use OKLCH to define your colors).

Say you have link tokens in your tokens.json:

json
{
  "icon": {
    "alert": {
      "$type": "link",
      "$value": "./icon/alert.svg"
    }
  }
}
yaml
icon:
  alert:
    $type: link
    value: ./icon/alert.svg

By default, consuming those will print values as-is:

scss
.icon-alert {
  background-image: token("icon.alert"); // url("./icon/alert.svg")
}

In some scenarios this is preferable, but in others, this may result in too many requests and may result in degraded performance. You can set embedFiles: true to generate the following instead:

scss
.icon-alert {
  background-image: token("icon.alert"); // url("image/svg+xml;utf8,<svg …></svg>");
}

TIP

The Sass plugin uses SVGO to optimize SVGs at lossless quality. However, raster images won’t be optimized so quality isn’t degraded.

Read more about the advantages to inlining files

Transform

Inside plugin options, you can specify an optional transform() function:

js
/** @type {import("@cobalt-ui/core").Config} */
export default {
  tokens: "./tokens.json",
  outDir: "./tokens/",
  plugins: [
    pluginSass({
      transform(token, mode) {
        const oldFont = "sans-serif";
        const newFont = "Custom Sans";
        if (token.$type === "fontFamily") {
          return token.$value.map((value) => (value === oldFont ? newFont : value));
        }
      },
    }),
  ],
};

Your transform will only take place if you return a truthy value, otherwise the default transformer will take place.

Custom tokens

If you have your own custom token type, e.g. my-custom-type, you’ll have to handle it within transform():

js
/** @type {import("@cobalt-ui/core").Config} */
export default {
  tokens: "./tokens.json",
  outDir: "./tokens/",
  plugins: [
    pluginSass({
      transform(token, mode) {
        switch (token.$type) {
          case "my-custom-type": {
            return String(token.$value);
            break;
          }
        }
      },
    }),
  ],
};