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$extends merges 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 transformersmix jetons.build uses a Jetons.Transformer behaviour; ship with Jetons.CSS.Transformer for 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 inspectionmix jetons.inspect for 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

FunctionDescription
token/1Get token value by path (returns nil if not found)
token!/1Get token value by path (raises KeyError if not found)
list_<category>/0List all tokens in a category as {path, value} tuples
group_by_path/2Group tokens by path depth into a nested map
get_groups/1Get sorted list of top-level groups for a category
get_in_group/2Get all tokens in a specific group as {key, value} tuples

Multi-Theme

All single-theme functions plus:

FunctionDescription
token/2Get token value with explicit theme parameter
token!/2Get token value with theme (raises if not found)
list_<category>/1List category tokens for a specific theme
themes/0List all available theme names
group_by_path/3Group tokens by path depth for a specific theme
get_groups/2Get top-level groups for a category and theme
get_in_group/3Get 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

FunctionDescription
from_config/1,2Parse a DTCG config map into {path, value} tuples
from_files/1,2Parse multiple grouped JSON files with cross-group reference resolution
extract_categories/1Extract sorted unique category names from token tuples

Jetons.DTCG — DTCG format operations

FunctionDescription
flatten/2Flatten a nested token map under a given prefix
apply_extends/1Resolve $extends inheritance in a config map
type_map/1Build a %{path => type} map with $type inheritance
descriptions/1Extract $description metadata from a config map
deprecated/1Extract $deprecated metadata from a config map

Jetons.Ref — Token reference expansion

FunctionDescription
expand/1Expand {ref} and #/pointer references in token tuples
ref?/1Check if a string is a brace reference
path/1Extract the dot-path from a brace reference
closest/2Find the closest match to a path using Jaro distance

Jetons.Transformer — Output format behaviour

FunctionDescription
init/1Initialize transformer state from config options
transform/3Transform raw DTCG config to {path, content} file tuples
flatten/1Helper (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

FunctionDescription
deep_merge/2Recursively 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.

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.css prefix — 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

FlagConfig keyDescription
-f / --fileresolver:Token or resolver JSON file (repeatable)
-m / --moduleModule that use Jetons (CLI only)
-o / --outputoutput:Output file path
--setset:Pin a modifier value (key=value, repeatable)
--inlineinline:Resolve token prefixes to literal values instead of var() references
--selectorselector:Override root CSS selector (default :root)
-t / --transformertransformer: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