CSS

Convert DTCG tokens into CSS variables for use in any web application or native app with webview. Convert your modes into any CSS selector for complete flexibility.

Setup

Requires Node.js 18 or later and the CLI installed. With both installed, run:

npm i -D @terrazzo/cli @terrazzo/plugin-css

And add it to terrazzo.config.js under plugins:

import pluginCSS from "@terrazzo/plugin-css";

/** @type {import("@terrazzo/cli").Config} */
export default {
  plugins: [
    pluginCSS({
      fileName: "tokens.css",
    }),
  ],
};

Lastly, run:

npx tz build

And you’ll output a tokens/tokens.css file (unless you renamed it) in your project. Import that anywhere in your project and use the CSS variables like you would normally!

Usage

This plugin outputs standard CSS variables that correspond directly to your token IDs. Use them as you would any CSS variable:

.button {
  color: var(--color-action-primary);
  font-family: var(--typography-family-default);
  font-size: var(--typography-font-size-200);
}

Features

CSS Color Module 4 support

This plugin lets you pick out-of-band colors in higher-gamut colorspaces like P3 and Rec2020 and automatically downconverts them to displayable colors using media queries. Use the colors you wanna; this plugin will just keep up.

If all your tokens are in the “safe” srgb color space, no extra code is generated. But when using colors in the P3 or Rec2020 gamut (or beyond), the CSS plugin automatically downconverts colors so they’re displayable on all hardware:

{
  "color": {
    "blue": {
      "600": {
        "$type": "color",
        "$value": {
          "colorSpace": "oklch",
          "channels": [0.5618, 0.227, 252.19], // Rec2020 color
        },
      },
    },
  },
}

The result is color that “just works” in any browser and hardware type automatically (and, yes, additional code is generated for modes, so this applies for all color modes you’re using!).

Color nerd info

Colors are downconverted using Culori’s toGamut() method which uses the same underlying math as CSS Color Level 4’s Gamut mapping algorithm and also described in Björn Ottosen’s sRGB Gamut Clipping article. This produces the best results for most applications on the web, using the best-available color research.

This is an improvement over Cobalt 1.x’s “expand into P3” method that oversaturated sRGB colors automatically unless opting out.

Utility CSS

Using the Tailwind integration isn’t necessary if you want to just have utility classes generated from your tokens. You can generate Tailwind-like utility CSS with minimal config.

Rather than scanning your code like Tailwind does, this takes a simpler approach by requiring you to manually specify your output groups. This keeps generated CSS minimal while outputting enough for you to handle all your styling needs from your tokens. Add a utility option to your config, and specify an object with key–value pairs of group: tokens:

import { defineConfig } from "@terrazzo/cli";
import css from "@terrazzo/plugin-css";
import { kebabCase } from "scule";

/** @type {import("@terrazzo/cli").Config} */
export default defineConfig({
  plugins: [
    css({
      utility: {
        bg: ["color.*-bg", "gradient.*"],
        border: ["border.*"],
        font: ["typography.*"],
        layout: ["space.*"],
        shadow: ["shadow.*"],
        text: ["color.*-text", "gradient.*"],
      },
    }),
  ],
});

Each of the keys are “groups,” which cut down on total CSS size. For example, consider all the possible ways dimension tokens could be used in CSS: margin, padding, gap, inset, font-size, to name a few! Rather than generate every possible property and every possible token (which would be a ton of CSS), you instead specify which tokens should belong to which groups (and they can belong to multiple).

tip

Only specifying the specific groups and tokens you need results in minimal CSS generated.

Group names are predefined, and only the following values are accepted. Each “group” will generate several CSS properties:

Border group

The border group accepts border tokens.

Group nameClassCSS
border.border-*border: [value]
.border-top-*border-top: [value]
.border-right-*border-right: [value]
.border-bottom-*border-bottom: [value]
.border-left-*border-left: [value]

Color groups

The color groups of bg and text accept color and gradient tokens.

Group nameClassCSS
bg.bg-*background-color: [value]
text.text-*color: [value]
tip

Improve your contrast by being more selective with what colors are allowed as background, and which as text colors (e.g. bg: ["color.*-bg"]). This can save lots of headaches when enforcing proper contrast!

Font group

The font group accepts font family, dimension (font size), font weight, and typography tokens.

Group nameClassCSS
font.font-*(all properties of Typography tokens)
note

The .font-* group is the most flexible! Be sure to pay attention to your token naming structure.

Layout group

The layout group accepts dimension tokens.

Group nameClassCSS
layout.gap-*gap: [value]
.gap-col-*column-gap: [value]
.gap-row-*row-gap: [value]
.mt-*margin-top: [value]
.mr-*margin-right: [value]
.mb-*margin-bottom: [value]
.ml-*margin-left: [value]
.ms-*margin-inline-start: [value]
.me-*margin-inline-end: [value]
.mx-*margin-left: [value]; margin-right: [value]
.my-*margin-top: [value]; margin-bottom: [value]
.ma-*margin: [value]
.pt-*padding-top: [value]
.pr-*padding-right: [value]
.pb-*padding-bottom: [value]
.pl-*padding-left: [value]
.px-*padding-left: [value]; padding-right: [value]
.py-*padding-top: [value]; padding-bottom: [value]
.pa-*padding: [value]

Shadow group

The shadow group accepts shadow tokens.

Group nameClassCSS
shadow.shadow-*box-shadow: [value]
note

All shadows will be interpreted as linear-gradient()s.

Differences from Tailwind

While the general philosophy is similar to Tailwind, this approach differs:

  • In naming. This plugin doesn’t map 1:1 with Tailwind names; it basically keeps your token names as-is and merely prefixes them. This results in a closer 1:1 mapping to your original design token names.
  • In mode support. This takes advantage of your mode selectors which need less configuration if modes are defined in tokens.json.
  • In simplicity. No code scanning is needed, and no heavy dependencies of PostCSS are needed. It generates only what you tell it to from your tokens.

Config

Configure options in terrazzo.config.js:

import css from "@terrazzo/plugin-css";
import { kebabCase } from "scule";

/** @type {import("@terrazzo/cli").Config} */
export default {
  plugins: [
    css({
      filename: "tokens.css",
      exclude: [], // ex: ["beta.*"] will exclude all tokens in the "beta" top-level group
      modeSelectors: [
        {
          mode: "light",
          selectors: [
            "@media (prefers-color-scheme: light)",
            '[data-mode="light"]',
          ],
        },
        {
          mode: "dark",
          selectors: [
            "@media (prefers-color-scheme: dark)",
            '[data-mode="dark"]',
          ],
        },
        { mode: "mobile", selectors: ["@media (width < 600px)"] },
        { mode: "desktop", selectors: ["@media (width >= 600px)"] },
        {
          mode: "reduced-motion",
          selectors: ["@media (prefers-reduced-motion)"],
        },
      ],
      variableName: (id) => kebabCase(id),
    }),
  ],
};
NameTypeDescription
filenamestringDefault filename (default: "tokens.css").
excludestring[]Glob pattern(s) of token IDs to exclude.
modeSelectorsModeSelector[]See modes.
variableName(id: string) => stringFunction that takes in a token ID and returns a CSS variable name. Use this if you want to prefix your CSS variables, or rename them in any way.
transform(token: TokenNormalized) => string | Record<string, string>Override certain token values by transforming them
utilityUtility CSS mappingGenerate Utility CSS from your tokens (docs

Mode Selectors

Mode selectors is the most powerful feature of the CSS plugin. It lets you convert your token modes into CSS media queries, classnames, or any CSS selector. To start, add a modeSelectors array to the CSS options. Every entry needs 2 things:

  1. The mode you’re targeting (this accepts globs, e.g. "*-light"!)
  2. The CSS selectors that enable these modes

For example, a common pattern for light and dark mode, with the following config, will generate the respective CSS:

import { defineConfig } from "@terrazzo/cli";
import css from "@terrazzo/plugin-css";

export default defineConfig({
  plugins: [
    css({
      modeSelectors: [
        {
          mode: "light",
          selectors: [
            "@media (prefers-color-scheme: light)",
            '[data-mode="light"]',
          ],
        },
        {
          mode: "dark",
          selectors: [
            "@media (prefers-color-scheme: dark)",
            '[data-mode="dark"]',
          ],
        },
      ],
    }),
  ],
});

Now, in your code, whenever you reference var(--color-blue-600), the value will depend on which media query is active, and/or which other selectors apply.

tip

The sky is the limit with mode selectors, but some popular patterns are:

transform()

transform() is a powerful tool that lets you override certain token values.

import { defineConfig } from "@terrazzo/cli";
import css from "@terrazzo/plugin-css";

export default defineConfig({
  plugins: [
    css({
      transform(token, mode) {
        if (token.id === "token.i.want" && mode === ".") {
          return "my-custom-value"; // generates `--token-i-want: my-custom-value;`
        }
      },
    }),
  ],
});

Its usage has changed slightly from Cobalt 1.x, because now it must return either a string or Record<string, string> value (docs):

  • Return a string to generate a single variable, e.g.
    :root {
      --duration-quick: 100ms;
    }
    
  • Return an object of strings to generate multiple variables, e.g. for then token typography.base and the keys fontFamily, fontSize, it would generate:
    :root {
      --typography-base-font-family: Inter;
      --typography-base-font-size: 1rem;
    }
    
  • Return undefined or null to fall back to the plugin’s default transformer (note that 0 and "" will take). You can also do this per-mode!
warning

Some token types that require multiple values (like typography) must return an object.

Migrating from Cobalt 1.x

For the most part, the 2.x version doesn’t have significant breaking changes and only improvements. But you’ll find the following minor differences:

  • sRGB colors don’t automatically expand into P3, which resulted in oversaturated (and innaccurate) colors. See CSS Color Module 4 Support for more details
  • The mode alias # character ({color.base.blue.600#dark}) has been deprecated (because it generated unpredictable CSS)
  • Colors now use the color() function so that it’s future-proof (supports deep color, wide color gamuts, and is overall a more future-friendly standard) while still maintaining good support (all modern browsers have great support).