Jetons
A compile-time design token library for Elixir.
Jetons generates fast, type-safe accessor functions from design token JSON files at compile time. Instead of runtime map lookups, tokens become individual function clauses that use pattern matching for maximum performance.
Features
- Compile-time generation — Tokens become pattern-matched functions, not data
- Multi-theme support — Light/dark/custom themes with zero runtime overhead
- DTCG 2025.10 format — Follows the Design Tokens Community Group specification
- Resolver support — Compose token files with sets, modifiers, and resolution ordering
- Token references —
{path.to.token}and JSON Pointer (#/path/to/token) resolution with circular-reference detection - Structural inheritance —
$extendsmerges parent groups into children with deep override semantics - Type-based transforms — Apply functions to tokens by their
$type(e.g. downcase all colors, convert px to rem) - Pluggable transformers —
mix jetons.builduses aJetons.Transformerbehaviour; ship withJetons.CSS.Transformerfor CSS custom properties, or implement your own for Swift, Kotlin, SCSS, etc. - CSS generation — Built-in CSS transformer produces custom properties with
var()references, multi-modifier diff blocks, structured value serialization (color spaces, dimensions, shadows, borders, transitions, gradients), composite utility classes, and custom modifier selectors via$extensions - Token inspection —
mix jetons.inspectfor debugging: lookup across contexts, reference chain tracing, modifier permutations, and context diffs - Fast lookups — O(1) access via pattern matching, not map traversal
Installation
Add jetons to your dependencies in mix.exs:
def deps do
[
{:jetons, "~> 0.1.1"},
{:jason, "~> 1.4"} # Required for JSON parsing
]
end
Quick Start
Single Theme
defmodule MyApp.Tokens do
use Jetons,
main: File.read!("tokens.json") |> Jason.decode!()
end
MyApp.Tokens.token("colors.primary") # => "#1B66B3"
MyApp.Tokens.token!("colors.primary") # => "#1B66B3" (raises if not found)
MyApp.Tokens.list_colors() # => [{"colors.primary", "#1B66B3"}, ...]
Multiple Themes
defmodule MyApp.Tokens do
use Jetons,
light: File.read!("tokens/light.json") |> Jason.decode!(),
dark: File.read!("tokens/dark.json") |> Jason.decode!()
end
MyApp.Tokens.token("colors.background") # Uses first theme (light)
MyApp.Tokens.token("colors.background", :dark) # Explicit theme
MyApp.Tokens.themes() # => [:light, :dark]
MyApp.Tokens.list_colors(:dark) # List colors for dark theme
Token Format
Jetons expects tokens in DTCG format: nested maps where leaf nodes have "$value" keys. Keys starting with $ ($type, $description, etc.) are metadata and filtered from the output.
{
"colors": {
"$type": "color",
"brand": {
"primary": { "$value": "#1B66B3" }
},
"grey": {
"500": { "$value": "#676767" }
}
},
"spacing": {
"small": { "$value": "8px" }
}
}
Nested keys become dot-notation paths:
"colors.brand.primary"→"#1B66B3""colors.grey.500"→"#676767""spacing.small"→"8px"
Token References
Tokens can reference other tokens using curly-brace syntax or JSON Pointers. References are resolved transitively, and circular references raise at compile time.
{
"colors": {
"palette": { "red": { "$value": "#FF0000" } },
"brand": { "$value": "{colors.palette.red}" },
"button": { "$value": "#/colors/brand" }
}
}
Both colors.brand and colors.button resolve to "#FF0000".
Structural Inheritance ($extends)
Groups can inherit tokens from a parent group using $extends. Child properties override inherited ones, and $extends chains are resolved transitively.
{
"colors": {
"base": {
"$type": "color",
"primary": { "$value": "#FF0000" }
},
"brand": {
"$extends": "colors.base",
"secondary": { "$value": "#00FF00" }
}
}
}
colors.brand inherits $type and primary from colors.base, and adds its own secondary. Cross-category extends (e.g. "brand.palette" extending "colors.base") is also supported.
Type-Based Transforms
The $type field propagates from parent groups to descendant tokens (a child can override with its own $type). You can then apply transform functions that target specific types:
defmodule MyApp.Tokens do
use Jetons,
transforms: [
{"color", fn v -> String.downcase(v) end},
{"dimension", fn v -> String.replace(v, "px", "rem") end}
],
main: %{
"colors" => %{
"$type" => "color",
"primary" => %{"$value" => "#FF0000"}
},
"spacing" => %{
"$type" => "dimension",
"small" => %{"$value" => "8px"}
}
}
end
MyApp.Tokens.token("colors.primary") # => "#ff0000"
MyApp.Tokens.token("spacing.small") # => "8rem"
Transforms run after reference resolution, so referenced values are transformed correctly. Tokens whose type doesn't match any transform pass through unchanged.
Generated API
Single Theme
| Function | Description |
|---|---|
token/1 | Get token value by path (returns nil if not found) |
token!/1 | Get token value by path (raises KeyError if not found) |
list_<category>/0 | List all tokens in a category as {path, value} tuples |
group_by_path/2 | Group tokens by path depth into a nested map |
get_groups/1 | Get sorted list of top-level groups for a category |
get_in_group/2 | Get all tokens in a specific group as {key, value} tuples |
Multi-Theme
All single-theme functions plus:
| Function | Description |
|---|---|
token/2 | Get token value with explicit theme parameter |
token!/2 | Get token value with theme (raises if not found) |
list_<category>/1 | List category tokens for a specific theme |
themes/0 | List all available theme names |
group_by_path/3 | Group tokens by path depth for a specific theme |
get_groups/2 | Get top-level groups for a category and theme |
get_in_group/3 | Get tokens in a group for a specific theme |
Runtime Functions
These modules are available for runtime use outside of use Jetons:
Jetons.Parser — Pipeline orchestrator
| Function | Description |
|---|---|
from_config/1,2 | Parse a DTCG config map into {path, value} tuples |
from_files/1,2 | Parse multiple grouped JSON files with cross-group reference resolution |
extract_categories/1 | Extract sorted unique category names from token tuples |
Jetons.DTCG — DTCG format operations
| Function | Description |
|---|---|
flatten/2 | Flatten a nested token map under a given prefix |
apply_extends/1 | Resolve $extends inheritance in a config map |
type_map/1 | Build a %{path => type} map with $type inheritance |
descriptions/1 | Extract $description metadata from a config map |
deprecated/1 | Extract $deprecated metadata from a config map |
Jetons.Ref — Token reference expansion
| Function | Description |
|---|---|
expand/1 | Expand {ref} and #/pointer references in token tuples |
ref?/1 | Check if a string is a brace reference |
path/1 | Extract the dot-path from a brace reference |
closest/2 | Find the closest match to a path using Jaro distance |
Jetons.Transformer — Output format behaviour
| Function | Description |
|---|---|
init/1 | Initialize transformer state from config options |
transform/3 | Transform raw DTCG config to {path, content} file tuples |
flatten/1 | Helper (via use Jetons.Transformer) to flatten config to token list |
Jetons.CSS.Transformer — Built-in CSS custom properties transformer (implements Jetons.Transformer)
Jetons.Map — Map utilities
| Function | Description |
|---|---|
deep_merge/2 | Recursively merge two maps (right side wins) |
Build Task
mix jetons.build generates output files from design tokens. It supports a config-based transformer architecture (recommended) and a legacy CLI mode.
Config-based (recommended)
Configure transformers in your application config:
config :jetons,
css: [
transformer: Jetons.CSS.Transformer,
resolver: "tokens.resolver.json",
set: [theme: "dark"],
output: "priv/static/tokens.css",
inline: ["color.palette"],
selector: ":root"
]
Then run:
mix jetons.build # Run all configured transformers
mix jetons.build css # Run a specific transformer
CLI flags override config values (e.g. --output, --selector).
Custom Transformers
Implement the Jetons.Transformer behaviour to generate any output format:
defmodule MyApp.IOSTransformer do
use Jetons.Transformer
def init(opts) do
{:ok, %{class_name: opts[:class_name] || "Tokens"}}
end
def transform(raw_config, opts, state) do
tokens = flatten(raw_config)
swift = generate_swift(tokens, state)
{:ok, [{opts[:output] || "Tokens.swift", swift}]}
end
end
config :jetons,
ios: [transformer: MyApp.IOSTransformer, class_name: "DesignTokens", output: "Tokens.swift"]
CLI mode (legacy)
The original CLI interface is still supported for quick one-off generation:
# Single JSON file
mix jetons.build -f tokens.json -o tokens.css
# Multiple files (merged in order)
mix jetons.build -f primitives.json -f semantics.json -o tokens.css
# From a module
mix jetons.build -m MyApp.Tokens -o tokens.css
# Custom transformer
mix jetons.build -f tokens.json -o tokens.css -t MyApp.IOSTransformer
Resolver files
Resolver files (.resolver.json) describe how to compose token files using sets and modifiers. Works in both config and CLI modes:
# config/config.exs
config :jetons, css: [resolver: "design.resolver.json", set: [brand: "edeka", theme: "dark"], ...]
mix jetons.build -f design.resolver.json -o tokens.css --set brand=markant
mix jetons.build -f design.resolver.json -o tokens.css --set brand=edeka --set theme=dark
When no --set flags are given, all modifier permutations are generated as diff-only CSS blocks:
:root {
--spacing-small: 8px;
--color-bg: #FFFFFF;
--color-text: #000000;
}
.theme-dark {
--color-bg: #000000;
--color-text: #FFFFFF;
}
Custom Modifier Selectors
Modifiers can specify custom CSS selectors and emit blocks for default contexts using $extensions:
{
"modifiers": {
"shade": {
"default": "dark",
"$extensions": {
"dev.jetons.css": {
"selector": ".shade-{context}, .shade-{context} *",
"emitDefault": true
}
},
"contexts": { "dark": [...], "light": [...] }
}
}
}
This produces .shade-dark, .shade-dark * { ... } (full block for the default) and .shade-light, .shade-light * { ... } (diff-only). Without $extensions, selectors default to .{modifier}-{context}.
Category Prefixes
CSS property names come straight from the token path (color.hue.300 → --color-hue-300). Figma exports each collection flat (hue, accent, …), which would emit --hue-300, --accent. To nest those groups under a category parent, tag the set or modifier that carries them with a dev.jetons.cssprefix — the group name to hoist under:
{
"sets": {
"primitives": {
"$extensions": { "dev.jetons.css": { "prefix": "color" } },
"sources": [{ "$ref": "palette.tokens.json" }]
}
},
"modifiers": {
"hue": {
"$extensions": { "dev.jetons.css": { "prefix": "color" } },
"contexts": { "blue": [...], "red": [...] }
}
}
}
At build time from_resolver/3 hoists every top-level group of each tagged source under color and rewrites the references that point at them — in memory, leaving the source token files in their flat Figma shape. The keys are derived automatically from each tagged source's top-level groups, so you never list them:
source (flat): hue.300 {hue.300} (a reference)
build-time nest: color.hue.300 {color.hue.300}
emitted CSS: --color-hue-300 var(--color-hue-300)
Untagged sets/modifiers are left alone, so groups already shaped as color.* (or other categories like spacing.*) keep their names. For one-off rewriting of the source files themselves, see mix jetons.namespace (with --unnest to reverse).
Composite Utility Classes
Typography ($type: "typography") and surface ($type: "x-surface") composite tokens render as CSS utility classes instead of custom properties:
{
"typography-display": {
"$type": "typography",
"$value": {
"fontFamily": "{font.display}",
"fontSize": "{text.display}",
"fontWeight": 900,
"lineHeight": "{text.display--line-height}",
"letterSpacing": "{text.display--letter-spacing}"
}
}
}
Generates:
.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);
}
See the CSS Generation guide for full documentation.
Options
| Flag | Config key | Description |
|---|---|---|
-f / --file | resolver: | Token or resolver JSON file (repeatable) |
-m / --module | — | Module that use Jetons (CLI only) |
-o / --output | output: | Output file path |
--set | set: | Pin a modifier value (key=value, repeatable) |
--inline | inline: | Resolve token prefixes to literal values instead of var() references |
--selector | selector: | Override root CSS selector (default :root) |
-t / --transformer | transformer: | Transformer module (CLI default: Jetons.CSS.Transformer) |
Token Inspection
Debug and explore tokens with mix jetons.inspect.
List Modifier Permutations
mix jetons.inspect -f design.resolver.json --permutations
Modifiers:
brand: edeka (default), markant
theme: light (default), dark
Permutations (4):
brand=edeka, theme=light
brand=edeka, theme=dark
brand=markant, theme=light
brand=markant, theme=dark
Look Up a Token Across Contexts
mix jetons.inspect -f design.resolver.json --token color.brand.primary
mix jetons.inspect -f design.resolver.json --token color.brand.primary --set brand=markant
Token: color.brand.primary
Type: color
brand=edeka, theme=light #1B66B3 (via {color.blue.700})
brand=edeka, theme=dark {color.blue.700} (unresolved)
brand=markant, theme=light #E63946 (via {color.red.500})
brand=markant, theme=dark {color.red.500} (unresolved)
Trace Reference Chains
mix jetons.inspect -f design.resolver.json --refs button.primary.color-background.default
button.primary.color-background.default
└─ color.background.brand.default
└─ color.brand.primary
└─ #1B66B3
Diff Between Contexts
# Diff defaults vs a specific context
mix jetons.inspect -f design.resolver.json --diff --set theme=dark
# Diff two explicit contexts
mix jetons.inspect -f design.resolver.json --diff --set brand=edeka --vs brand=markant
Diff: brand=edeka, theme=light → brand=edeka, theme=dark
31 token(s) changed
color.background.default {color.utility.white} → {color.grey.950}
color.background.subtle {color.grey.50} → {color.grey.900}
color.content.default {color.grey.900} → {color.grey.50}
Performance
Jetons generates individual function clauses for each token:
def token("colors.primary"), do: "#1B66B3"
def token("colors.secondary"), do: "#FCE531"
# ... one clause per token
def token(_), do: nil
This means:
- Token access is O(1) — Direct function call with pattern matching
- No runtime overhead — All processing happens at compile time
- Memory efficient — Functions are code, not data
License
MIT