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
pnpm 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
},
},
},
},
}
:root {
--color-blue-600: oklch(0.5618 0.1686 252.19); /* srgb safe color */
}
@media (color-gamut: p3) {
:root {
--color-blue-600: oklch(0.5618 0.2171 252.19); /* p3 safe color */
}
}
@media (color-gamut: rec2020) {
:root {
--color-blue-600: oklch(0.5618 0.227 252.19);
}
}
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).
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 name | Class | CSS |
---|---|---|
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 name | Class | CSS |
---|---|---|
bg | .bg-* | background-color: [value] |
text | .text-* | color: [value] |
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 name | Class | CSS |
---|---|---|
font | .font-* | (all properties of Typography tokens) |
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 name | Class | CSS |
---|---|---|
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 name | Class | CSS |
---|---|---|
shadow | .shadow-* | box-shadow: [value] |
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),
}),
],
};
Name | Type | Description |
---|---|---|
filename | string | Default filename (default: "tokens.css" ). |
exclude | string[] | Glob pattern(s) of token IDs to exclude. |
modeSelectors | ModeSelector[] | See modes. |
variableName | (id: string) => string | Function 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 |
utility | Utility CSS mapping | Generate 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:
- The
mode
you’re targeting (this accepts globs, e.g."*-light"
!) - 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"]',
],
},
],
}),
],
});
:root {
--color-blue-600: #0588f0;
}
@media (prefers-color-scheme: light) {
:root {
--color-blue-600: #0588f0;
}
}
[data-mode="light"] {
--color-blue-600: #0588f0;
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue-600: #3b9eff;
}
}
[data-mode="dark"] {
--color-blue-600: #3b9eff;
}
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.
The sky is the limit with mode selectors, but some popular patterns are:
color
: prefers-color-scheme, prefers-contrastduration
: prefers-reduced-motiontypography
: viewport width (responsive styles)
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 keysfontFamily
,fontSize
, it would generate::root { --typography-base-font-family: Inter; --typography-base-font-size: 1rem; }
- Return
undefined
ornull
to fall back to the plugin’s default transformer (note that0
and""
will take). You can also do this per-mode!
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).