CSS Integration
Generate CSS variables from your Design Tokens Community Group (DTCG) tokens.
This plugin generates CSS variables for dynamic, flexible theming that supports modes and gives you the full range of what CSS can do.
Setup
TIP
Make sure you have the Cobalt CLI installed!
Install the plugin:
npm i -D @cobalt-ui/plugin-css
Then add to your tokens.config.mjs
file:
import pluginCSS from "@cobalt-ui/plugin-css";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [pluginCSS()],
};
And run:
npx co build
You’ll then get a ./tokens/tokens.css
file with CSS variables for you to use anywhere in your app:
:root {
--color-blue: #0969da;
--color-green: #2da44e;
--color-red: #cf222e;
--color-black: #101010;
--color-ui-text: var(--color-black);
}
Config
Here are all plugin options, along with their default values
import pluginCSS from "@cobalt-ui/plugin-css";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
pluginCSS({
/** set the filename inside outDir */
filename: "./tokens.css",
/** create selector wrappers around modes */
modeSelectors: [
// …
],
/** embed file tokens? */
embedFiles: false,
/** (optional) transform specific token values */
transform: () => null,
/** (deprecated, use generateName instead) add custom namespace to CSS vars */
prefix: "",
/** enable P3 support? */
p3: true,
/** normalize all colors */
colorFormat: "hex",
/** used to generate the name of each CSS variable */
generateName: defaultNameGenerator,
}),
],
};
VS Code Autocomplete
If your tokens are saved locally (by default in src/tokens/tokens.css
), VS Code will automatically pick up on this file and allow autocompletions. However, if you’re publishing your package to npm, it will ignore node_modules
. The CSS Variable Autocomplete extension lets you add additional files for autocompletion.
Utility CSS
By default, this plugin will only generate CSS variables. To generate some lightweight utility CSS classes from your tokens a la Tailwind or Bootstrap Utility CSS, specify a utility
object to enable the types of utility classes you’d like to generate.
By default, all groups are off. to generate a group, pass its name as the key, along with an array of token selectors (wildcards) to match tokens. For example, the following config:
pluginCSS({
utility: {
bg: ["color.semantic.*"],
text: ["color.semantic.*"],
margin: ["space.*"],
},
});
…will generate the following CSS:
.bg-primary {
background-color: var(--color-semantic-primary);
}
.bg-secondary {
background-color: var(--color-semantic-secondary);
}
.text-primary {
color: var(--color-semantic-primary);
}
.text-secondary {
color: var(--color-semantic-secondary);
}
.mt-1 {
margin-top: 0.25rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
/* … */
Here are all the groups available, along with the associated CSS:
Group | Class Name | CSS |
---|---|---|
bg | .bg-[token] | background-color: [value] * |
border | .border-[token] | border: [value] |
.border-top-[token] | border-top: [value] | |
.border-right-[token] | border-right: [value] | |
.border-bottom-[token] | border-bottom: [value] | |
.border-left-[token] | border-left: [value] | |
font | .font-[token] | (all typographic properties of Typography Tokens) |
gap | .gap-[token] | gap: [value] |
.gap-col-[token] | column-gap: [value] | |
.gap-row-[token] | row-gap: [value] | |
margin | .mt-[token] | margin-top: [value] |
.mr-[token] | margin-right: [value] | |
.mb-[token] | margin-bottom: [value] | |
.ml-[token] | margin-left: [value] | |
.ms-[token] | margin-inline-start: [value] | |
.me-[token] | margin-inline-end: [value] | |
.mx-[token] | margin-left: [value]; margin-right: [value] | |
.my-[token] | margin-top: [value]; margin-bottom: [value] | |
.ma-[token] | margin: [value] | |
padding | .pt-[token] | padding-top: [value] |
.pr-[token] | padding-right: [value] | |
.pb-[token] | padding-bottom: [value] | |
.pl-[token] | padding-left: [value] | |
.px-[token] | padding-left: [value]; padding-right: [value] | |
.py-[token] | padding-top: [value]; padding-bottom: [value] | |
.pa-[token] | padding: [value] | |
shadow | .shadow-[token] | box-shadow: [value] |
text | .text-[token] | color: [value] * |
INFO
The bg and text groups also accept Gradient Tokens, and will generate the appropriate CSS for those.
Naming
The utility
mapping will use the remainder of the token ID, minus the selector (but will always keep the last segment, no matter what). For example, if you had a color.semantic.primary
token, here’s how you’d control the generated CSS name:
Selector | CSS Class |
---|---|
["color.semantic.primary"] | .bg-primary |
["color.semantic.*"] | .bg-primary |
["color.*"] | .bg-semantic-primary |
["*"] | .bg-color-semantic-primary |
You can use as much or as little of the token ID as you like, according to what makes sense to you.
This comes up a lot with spacing (Dimension) tokens: if, for example, you had a space.layout.xs
token, you could specify ["space.*"]
if you wanted the CSS class .mt-layout-xs
, or ["space.layout.*"]
if you wanted .mt-xs
. Only you know your DS and what makes the most sense, and when a name is either too long or too short.
Note that this utility does not let you rename token IDs for ease of use. If you want to remap and/or mix and combine tokens into different class names, you’ll have to write your own CSS manually (using the generated CSS variables, of course).
Comparison to Tailwind
This plugin’s utility CSS can be used in place of Tailwind, and probably works best if the project isn’t based on Tailwind. It’s simply a lighter-weight way of using your design tokens directly in CSS. For comparison:
- ✅ It respects dynamic vars. The generated utility CSS references core vars, which means all your modes and Mode Selectors are preserved, and all the dynamism of your variables are kept.
- ✅ Direct 1:1 mapping with tokens. There’s no additional translation layer, or renaming into Tailwind. This just references your tokens as you’ve named them (with the only learning curve being familiarizing yourself with a few prefixes).
- ✅ No additional build step or dependencies. Utility CSS gets generated along with the rest of your DS code, without any additional setup.
- ✅ No code scanning. You only generate what you need, so no need to scan your code.
- ❌ No automatic treeshaking. Conversely, you control everything in the config, so you’ll have to configure for yourself how much CSS to generate from your DS (for most DSs it’s a negligible amount of CSS, however, huge DSs may need to be more selective).
If you are already using Tailwind in your project, you may find the Tailwind Plugin more useful.
Renaming CSS variables
Use the generateName()
option to customize the naming of CSS tokens, such as adding prefixes/suffixes, or just changing how the default variable naming works in general.
Default naming
By default, Cobalt takes your dot-separated token IDs and…
- Removes leading and trailing whitespace from each group or token name in an ID
- camelCases any group or token name that has a space in the middle of it
- Joins the normalized segments together with a single dashes
Custom naming
To override specific or all CSS variable names yourself, use the generateName()
option:
import pluginCSS from "@cobalt-ui/plugin-css";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
pluginCSS({
generateName(variableId, token) {
if (variableId === "my.special.token") {
return "SUPER_IMPORTANT_VARIABLE";
}
// if nothing returned, fall back to default behavior
},
}),
],
};
A couple things to be aware of:
token
can beundefined
in rare cases- This occurs when a token references another token that is not defined. Currently, this is not explicitly disallowed by the design tokens specification.
variableId
may not be a 1:1 match with thetoken.id
- For example, each property in a composite token will have its own variable generated, so those
variableId
s will include the property name. In most cases you should usevariableId
rather thantoken.id
.
- For example, each property in a composite token will have its own variable generated, so those
- The string returned does not need to be prefixed with
--
, Cobalt will take care of that for you
Modes
To generate CSS for Modes, add a modeSelectors
array to your config that specifies the mode you’d like to target and which CSS selectors should activate those modes (can either be one or multiple). You may optionally also decide to include or exclude certain tokens (e.g. color.*
will only target the tokens that begin with color.
).
import css from "@cobalt-ui/plugin-css";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
css({
modeSelectors: [
{
mode: "light", // match all tokens with $extensions.mode.light
selectors: ["@media (prefers-color-scheme: light)", '[data-color-mode="light"]'], // the following CSS selectors trigger the mode swap
tokens: ["color.*"], // (optional) limit to specific tokens, if desired (by default any tokens with this mode will be included)
},
{
mode: "dark",
selectors: ["@media (prefers-color-scheme: dark)", '[data-color-mode="dark"]'],
tokens: ["color.*"],
},
{
mode: "reduced",
selectors: ["@media (prefers-reduced-motion)"],
},
],
}),
],
};
This would generate the following CSS:
:root {
/* all default token values (ignoring modes) */
}
@media (prefers-color-scheme: light) {
:root {
/* all `light` mode values for color.* tokens */
}
}
[data-color-mode="light"] {
/* (same) */
}
/* dark theme colors */
@media (prefers-color-scheme: dark) {
:root {
/* all `dark` mode values for color.* tokens */
}
}
[data-color-mode="dark"] {
/* (same) */
}
@media (prefers-reduced-motion) {
:root {
/* all `reduced` mode values for any token */
}
}
In our example the @media
selectors would automatically pick up whether a user’s OS is in light or dark mode. But as a fallback, you could also manually set data-color-mode="[mode]"
on any element override the default (e.g. for user preferences, or even previewing one theme in the context of another).
Further, any valid CSS selector can be used (that’s why it’s called modeSelectors
and not modeClasses
)! You could also generate CSS if your typography.size
group had desktop
and mobile
sizes:
import css from "@cobalt-ui/plugin-css";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
css({
modeSelectors: [
{ mode: "mobile", tokens: ["typography.size.*"], selectors: ["@media (width < 600px)"] },
{ mode: "desktop", tokens: ["typography.size.*"], selectors: ["@media (width >= 600px)"] },
],
}),
],
};
That will generate the following:
:root {
/* all tokens (defaults) */
}
@media (width < 600px) {
:root {
/* `mobile` mode values for `typography.size.*` tokens */
}
}
@media (width >= 600px) {
:root {
/* `desktop` mode values for `typography.size.*` tokens */
}
}
Transforming values
Inside plugin options, you can specify an optional transform()
function.
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
pluginCSS({
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()
:
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
pluginCSS({
transform(token, mode) {
switch (token.$type) {
case "my-custom-type": {
return String(token.$value);
break;
}
}
},
}),
],
};
Special token behavior
Helpful information for @cobalt-ui/plugin-css’ handling of specific token types.
Color tokens
/** @type {import("@cobalt-ui/core").Config} */
export default {
plugins: [
pluginCSS({
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).
Link tokens
Say you have Link tokens in your tokens.json
:
{
"icon": {
"alert": {
"$type": "link",
"$value": "./icon/alert.svg"
}
}
}
icon:
alert:
$type: link
value: ./icon/alert.svg
By default, consuming those will print values as-is:
:root {
--icon-alert: url("./icon/alert.svg");
}
.icon-alert {
background-image: var(--icon-alert);
}
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:
:root {
--icon-alert: url("image/svg+xml;utf8,<svg …></svg>");
}
.icon-alert {
background-image: var(--icon-alert);
}
TIP
The CSS 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
Sass typechecking
If you’re using Sass in your project, you can load this plugin through @cobalt-ui/plugin-sass, which lets you keep the dynamism of CSS variables but lets Sass check for typos (by default, the Sass plugin uses static values).
To use this, replace this plugin with @cobalt-ui/plugin-sass in tokens.config.mjs
and move your options into the pluginCSS: {}
option:
import pluginCSS from "@cobalt-ui/plugin-css";
import pluginSass from "@cobalt-ui/plugin-sass";
/** @type {import("@cobalt-ui/core").Config} */
export default {
tokens: "./tokens.json",
outDir: "./tokens/",
plugins: [
pluginCSS({filename: "tokens.css"}),
pluginSass({
pluginCSS: {filename: "tokens.css"},
}),
],
};
To learn more, read the docs.