Jetons generates CSS custom properties from DTCG token files. It handles literal values, token references as var(), structured value types (colors, dimensions, shadows, etc.), multi-modifier diff blocks, composite utility classes, and custom modifier selectors.
Entry Points
There are three ways to generate CSS:
| Function | Input | Use case |
|---|---|---|
Jetons.CSS.generate/2 | Module that use Jetons | Compile-time tokens |
Jetons.CSS.from_config/2 | Raw DTCG config map | Single file, no resolver |
Jetons.CSS.from_resolver/3 | Resolver document + base dir | Multi-file, multi-modifier |
The mix jetons.build task wraps these behind a config-driven transformer pipeline.
Basic Output
A flat token file:
{
"color": {
"brand": { "$value": "#1B66B3" },
"text": { "$value": "{color.brand}" }
},
"spacing": {
"small": { "$value": "8px" }
}
}Produces:
:root {
--color-brand: #1B66B3;
--color-text: var(--color-brand);
--spacing-small: 8px;
}References become var() calls. The :root selector is configurable via the :selector option.
Inlining
The :inline option takes a list of path prefixes. Tokens matching these prefixes are omitted from the output, and references to them resolve to literal values instead of var().
Jetons.CSS.from_config(config, inline: ["color.palette"])This is useful for primitive scales that shouldn't appear as custom properties but whose values should be inlined wherever referenced.
Structured Value Types
Jetons serializes DTCG structured values to their CSS equivalents:
| DTCG type | Example $value | CSS output |
|---|---|---|
| Color (sRGB) | {"colorSpace": "srgb", "components": [1, 0, 0]} | color(srgb 1 0 0) |
| Color (HSL) | {"colorSpace": "hsl", "components": [330, 100, 50]} | hsl(330 100% 50%) |
| Color (OKLCH) | {"colorSpace": "oklch", "components": [0.7, 0.32, 328]} | oklch(0.7 0.32 328) |
| Dimension | {"value": 8, "unit": "px"} | 8px |
| Duration | {"value": 200, "unit": "ms"} | 200ms |
| Font family | ["Helvetica Neue", "Arial", "sans-serif"] | "Helvetica Neue", "Arial", sans-serif |
| Cubic bezier | [0.42, 0, 1, 1] | cubic-bezier(0.42, 0, 1, 1) |
| Number | 1.5 | 1.5 |
| Shadow | {"offsetX": ..., "offsetY": ..., "blur": ..., "spread": ..., "color": ...} | 2px 2px 4px 0px color(...) |
| Border | {"width": ..., "style": ..., "color": ...} | 3px solid color(...) |
| Transition | {"duration": ..., "delay": ..., "timingFunction": ...} | 200ms 0ms cubic-bezier(...) |
| Gradient | [{"color": ..., "position": 0}, ...] | linear-gradient(color(...) 0%, ...) |
Alpha channels are included when present and not equal to 1. The "none" keyword is preserved for color components.
Multi-Modifier Output
from_resolver/3 generates a base :root block using default modifier values, then a diff-only block for each non-default context. Only tokens whose values actually differ from the base appear in diff blocks.
{
"modifiers": {
"theme": {
"default": "light",
"contexts": {
"light": [{"$ref": "light.json"}],
"dark": [{"$ref": "dark.json"}]
}
}
}
}:root {
--color-bg: #FFFFFF;
--color-text: #000000;
}
.theme-dark {
--color-bg: #000000;
--color-text: #FFFFFF;
}Pinning modifiers via the :input option bakes them into the base resolution and skips their expansion:
Jetons.CSS.from_resolver(doc, dir, input: %{"brand" => "edeka"})Custom Set Selectors
Sets can also use $extensions.dev.jetons.css to emit their tokens under a custom selector, separate from the :root block. This is useful for CSS at-rules like @theme (Tailwind v4) or @layer where tokens need to be registered at a specific cascade level.
{
"sets": {
"hue-defaults": {
"$extensions": {
"dev.jetons.css": {
"selector": "@theme"
}
},
"sources": [{"$ref": "hue_grey.json"}]
}
}
}This produces:
@theme {
--color-hue-50: var(--grey-50);
--color-hue-500: var(--grey-500);
/* ... neutral defaults */
}
:root {
/* ... full resolution including modifier overrides */
}Set extension blocks appear before the :root block in the output. The set's tokens are still included in the full resolution (so modifiers can override them), but the set block itself only contains the tokens defined by that set's sources.
References within the set are resolved against the full base config, not just the set in isolation. This means a set can reference tokens from other sets (e.g. {grey.500} resolves to var(--grey-500)) without needing to include those sets in its own sources.
Custom Modifier Selectors
By default, modifier diff blocks use .{modifier}-{context} as their selector. The $extensions key on a modifier definition lets you customize this.
Selector Template
The selector field is a template string where {context} is replaced with the context name:
{
"modifiers": {
"shade": {
"default": "dark",
"$extensions": {
"dev.jetons.css": {
"selector": ".shade-{context}, .shade-{context} *"
}
},
"contexts": {
"dark": [{"$ref": "shade_dark.json"}],
"light": [{"$ref": "shade_light.json"}]
}
}
}
}This produces:
.shade-light, .shade-light * {
--color-bg: #FFFFFF;
/* only tokens that differ from dark (the default) */
}The descendant combinator (*) is a common pattern for design systems where shade should cascade to all children.
Emitting the Default Context
Normally the default context is only emitted under :root. Setting emitDefault to true generates an additional block for the default context under its named selector:
{
"$extensions": {
"dev.jetons.css": {
"selector": ".hue-{context}",
"emitDefault": true
}
}
}With "default": "blue", this produces both :root { ... } (base resolution) and .hue-blue { ... } (full block with same values). This is useful when the default needs to work as an explicit class selector, not just as the implicit :root state.
Extension Namespace
Extensions use the dev.jetons.css vendor namespace under the standard DTCG $extensions mechanism (spec section 4.3). This keeps the configuration portable -- tools that don't understand the extension simply ignore it.
Composite Utility Classes
Two composite token types render as CSS utility class blocks instead of custom properties:
Typography ($type: "typography")
The standard DTCG typography composite type (spec section 9.8). Sub-values map to CSS properties:
| DTCG key | CSS property |
|---|---|
fontFamily | font-family |
fontSize | font-size |
fontWeight | font-weight |
letterSpacing | letter-spacing |
lineHeight | line-height |
{
"typography-display": {
"$type": "typography",
"$value": {
"fontFamily": "{font.display}",
"fontSize": "{text.display}",
"fontWeight": 900,
"lineHeight": "{text.display--line-height}",
"letterSpacing": "{text.display--letter-spacing}"
}
}
}.typography-display {
font-family: var(--font-display);
font-size: var(--text-display);
font-weight: 900;
letter-spacing: var(--text-display--letter-spacing);
line-height: var(--text-display--line-height);
}Reference sub-values ("{font.display}") become var() calls. Literal sub-values (numbers, strings) are output directly. The token path becomes the class name with dots replaced by dashes.
Surface ($type: "x-surface")
A non-standard composite type for pairing background and text colors. The x- prefix follows the convention for vendor-specific extensions to avoid collision with future DTCG types.
| DTCG key | CSS property |
|---|---|
background | background |
color | color |
{
"surface-background": {
"$type": "x-surface",
"$value": {
"background": "{color.surface.background}",
"color": "{color.text.on-background}"
}
}
}.surface-background {
background: var(--color-surface-background);
color: var(--color-text-on-background);
}Behavior in Modifier Diff Blocks
Utility class tokens are excluded from modifier diff blocks. Since their sub-values reference other tokens via var(), they inherit the correct values automatically when the referenced custom properties change. The utility classes only appear once in the base block output.
Dangling References
Composite sub-values can reference tokens that don't exist in the token system. For example, "{font.display}" renders as var(--font-display) even if no font.display token is defined. This is intentional -- font-family values often come from outside the design token system (web font loaders, CSS @font-face rules, etc.).
Transformer
The Jetons.CSS.Transformer module implements the Jetons.Transformer behaviour for use with mix jetons.build. It produces the same output as Jetons.CSS.from_config/2 and supports the same utility class and inlining features.
config :jetons,
css: [
transformer: Jetons.CSS.Transformer,
resolver: "spot/spot.resolver.json",
output: "priv/static/tokens.css",
inline: ["palette"],
selector: ":root"
]