DTCG 2025.10 design token format — strongly-typed F# library
  • F# 97.1%
  • CSS 2.5%
  • Shell 0.4%
Find a file
IvanTheGeek f290a7f314
All checks were successful
CI / build-test (push) Successful in 54s
Publish dev package / publish (push) Successful in 46s
Merge chore/runner-label-and-toolchain-diagnostics (ms-ubuntu-net10sdk + toolchain diagnostics)
2026-05-18 11:02:58 -04:00
.claude chore: adopt workspace-canonical Stop hook (decision 019e34a8-...) 2026-05-17 03:27:22 -04:00
.forgejo/workflows chore(ci): switch runs-on to ms-ubuntu-net10sdk and report toolchain versions 2026-05-18 11:02:44 -04:00
docs docs: add mermaid diagrams where they clarify ASCII / tabular content 2026-05-11 19:34:44 -04:00
LOGOS docs: add mermaid diagrams where they clarify ASCII / tabular content 2026-05-11 19:34:44 -04:00
samples docs(phase3): add gaps 5-6 and parse error analysis to phase3-findings 2026-05-03 21:55:20 -04:00
scripts fix: alias $type coercion, typeless alias shim, tablet @media 2026-05-04 13:46:31 -04:00
src release: 0.13.0 — ResolvedTokens type alias 2026-05-11 15:15:55 -04:00
tests/FnTools.DesignTokens.Tests release: 0.12.0 — rename Bindings → FSharp; add concept doc + ADR-039 2026-05-11 14:32:14 -04:00
.gitignore chore: add .claude/ to .gitignore to prevent worktree state leaking into commits 2026-05-16 23:56:41 -04:00
AGENTS.md chore: turn-end safety net 2026-05-17T03:40:23Z 2026-05-16 23:40:23 -04:00
AGENTS.source.md chore: turn-end safety net 2026-05-17T03:40:23Z 2026-05-16 23:40:23 -04:00
CLAUDE.md chore: turn-end safety net 2026-05-17T03:40:23Z 2026-05-16 23:40:23 -04:00
CONTRIBUTING.md release: 0.12.0 — rename Bindings → FSharp; add concept doc + ADR-039 2026-05-11 14:32:14 -04:00
FnTools.DesignTokens.slnx release: 0.12.0 — rename Bindings → FSharp; add concept doc + ADR-039 2026-05-11 14:32:14 -04:00
LICENSE chore: relocate repo to FnTools.DesignTokens workspace 2026-05-01 22:15:12 -04:00
llms.txt release: 0.13.0 — ResolvedTokens type alias 2026-05-11 15:15:55 -04:00
publish.sh chore: publish.sh — flag tag-triggered CI as the preferred stable path 2026-05-10 20:29:23 -04:00
README.md release: 0.13.0 — ResolvedTokens type alias 2026-05-11 15:15:55 -04:00
WORKSPACE chore: turn-end safety net 2026-05-17T03:40:23Z 2026-05-16 23:40:23 -04:00

FnTools.DesignTokens

F# library implementing the DTCG 2025.10 specification — the W3C Community Group standard for sharing design tokens between tools. Parses, resolves, and translates design-token files into any target format your codebase needs: CSS custom properties, F# source code, Tokens Studio JSON, and more.

New here? Read docs/concept.md first — one page on what this library is, where it sits in the design-token toolchain, and what it deliberately does not do.


Where this library sits in the pipeline

The DTCG community describes the design-token toolchain as four stages:

DTCG source → Resolver → Translator → Target

This library fills the Resolver and Translator stages. It does not author tokens (a design tool does that) and it does not run a UI (your application does that).

  • Resolver stage — accepts a DTCG token file, Tokens Studio JSON, or a .resolver.json; merges multi-set inheritance chains; evaluates math expressions; resolves aliases; produces a flat sequence of (path, resolved-value) pairs.
  • Translator stage — turns that flat sequence into a target-native string: CSS custom properties, F# constants, Penpot-compatible JSON, and (planned) Swift, Kotlin, XAML for native platforms.

The boundary between these two stages is a single type:

type ResolvedTokens = (string list * ResolvedToken) seq

Every emitter consumes this. Add a new platform target by adding a new emitter package — the core does not change. See ADR-039: Emitter contract and naming.

Strings in, strings out. The library never touches the filesystem; the caller owns I/O (see ADR-003).


What it does

Parse and validate DTCG 2025.10 token files. All 13 token types. Alias chains, composite values, group inheritance. Errors accumulate rather than short-circuit — you get all problems at once.

Resolve multi-set token files using a resolver document — merge sets in resolution order, apply modifier contexts (themes, breakpoints, brands), flatten to a concrete token list.

Shim Tokens Studio format — convert Tokens Studio multi-set JSON to DTCG-compatible form:

  • Type renames (fontFamiliesfontFamily, spacingdimension, etc.)
  • Math expression evaluation (round({base} * pow({multiplier}, 2)) → a concrete float)
  • HSL expression evaluation (hsla({hue.blue},{saturation},{lightness.600},1)#3d7ab5)
  • Typography composite field normalisation
  • $themes / $metadata extraction for theme-aware workflows

Emit CSS from resolved tokens — :root {} blocks, per-theme override blocks ([data-theme="dark"] {}), responsive @media blocks, calc() expressions that preserve mathematical relationships, per-path unit policy (e.g. font-size.* tokens in rem).

Emit typed F# bindings — a Tokens module of string constants with var(--token-name) values, usable directly in Fun.Css.

Export back to Tokens Studio — preserve aliases, TS type names, HSL expressions, and combined fontWeight strings. The round-trip is lossless for everything the format can represent.

Ingest CSS — extract custom properties from existing CSS files, infer token types, produce DTCG token files. The migration path from a hand-authored CSS design system.

Audit CSS — scan stylesheets for hardcoded values not covered by existing tokens; flag duplicates of values already covered by tokens.


Package layout

Eight packages, all in one meta-package:

Package Stage What it does
FnTools.DesignTokens.Foundation core Domain types. No external dependencies.
FnTools.DesignTokens.Format resolver JSON parsing and serialization.
FnTools.DesignTokens.Validation resolver Strict-by-default constraint checks; permissive mode opt-in.
FnTools.DesignTokens.Resolver resolver Multi-set merging, alias resolution, math expression evaluation.
FnTools.DesignTokens.Css translator CSS custom-property emitter, calc-preserving emission, themed emission, CSS ingest + audit.
FnTools.DesignTokens.FSharp translator F# source emitter; produces typed token modules. (Renamed from Bindings in v0.12.0.)
FnTools.DesignTokens.TokensStudio translator Tokens Studio import/export with alias-preserving round-trip.
FnTools.DesignTokens meta Re-exports all of the above; one reference gets everything.

Each translator package depends only on Foundation. A consumer that only emits Swift will eventually reference only Foundation + Swift. See ADR-001.


Install

Hosted on a self-managed Forgejo feed, not NuGet.org. Add the source first:

nuget.config (place alongside your .sln / .fsproj / .fsx):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="FnTools" value="https://forgejo.ivanthegeek.com/api/packages/FnTools/nuget/index.json" />
  </packageSources>
</configuration>

.NET CLI

dotnet add package FnTools.DesignTokens --version 0.13.0

PackageReference

<PackageReference Include="FnTools.DesignTokens" Version="0.13.0" />

F# script (#r)

#r "nuget: FnTools.DesignTokens, 0.13.0"

One reference gets everything. The sub-packages (Foundation, Format, Validation, Resolver, Css, FSharp, TokensStudio) are published separately if you need only specific layers.


Quick start

open FnTools.DesignTokens

// Plain DTCG JSON → resolved tokens
let json    = System.IO.File.ReadAllText "tokens.json"
let tokens  = Api.import json |> Result.get   // ResolvedTokens

// Resolved tokens → CSS
let css = FnTools.DesignTokens.Css.CssEmitter.emit tokens
System.IO.File.WriteAllText("tokens.css", css)

// Resolved tokens → F# bindings module
let source = FnTools.DesignTokens.FSharp.emit "Tokens" tokens
System.IO.File.WriteAllText("Tokens.fs", source)
// Tokens Studio JSON → themed CSS (math expressions evaluated, aliases preserved)
let tsJson = System.IO.File.ReadAllText "tokens.json"
let result = Api.importTokensStudioThemed ShimConfig.Default ["Light"; "Dark"] tsJson |> Result.get
let css =
    FnTools.DesignTokens.Css.CssEmitter.emitThemedWith
        (fun path unit -> match path with "font-size" :: _ -> Rem | _ -> unit)
        (fun theme -> $"[data-theme=\"{theme}\"]")
        result.BaseTokens
        result.Themes
// Round-trip: read Tokens Studio → modify → export back to Penpot
let raw = Api.importTokensStudioRaw ShimConfig.Default tsJson |> Result.get
// ... inspect or modify raw.Import.Tokens ...
let (penpotJson, warnings) = Api.exportTokensStudio raw.ShimResult raw.ParsedSets

The five-minute walkthrough lives in docs/getting-started.md.


Docs


License

AGPL-3.0