n5s / dtcg-tokens
PHP-first runtime library for DTCG design tokens, with Twig and Symfony bridges
Requires
- php: ^8.4
- ozdemirburak/iris: ^4.1.1
- psr/cache: ^3.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.50
- phpstan/phpstan: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12 || ^13
- rector/rector: ^2.4
- shipmonk/phpstan-rules: ^4.4
- symfony/cache: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symplify/easy-coding-standard: ^13.0
- twig/twig: ^3.0
Suggests
- symfony/http-kernel: For the Symfony bundle (auto-wiring + Twig registration)
- twig/twig: For Twig template integration via TokenExtension
This package is auto-updated.
Last update: 2026-06-16 17:42:03 UTC
README
Read, resolve, and render DTCG design tokens at runtime in PHP. It implements the Design Tokens Community Group specification and is PHP-first, with optional Twig and Symfony bridges.
Install
composer require n5s/dtcg-tokens
Requires PHP 8.4 or newer.
Quick start (plain PHP)
use n5s\DtcgTokens\Tokens; $tokens = Tokens::fromFile('tokens.json'); $tokens->get('color.primary'); // ColorValue (Stringable) (string) $tokens->get('space.md'); // "16px" $tokens->get('color.primary', 'dark'); // mode-aware lookup
Tokens is an immutable, iterable (IteratorAggregate) and countable collection keyed by dot-separated token path. Build it from one or many files, or from an already-decoded array:
Tokens::fromFile('tokens.json'); // single file Tokens::fromFiles(['base.json', 'overrides.json']); // merged, later files win Tokens::fromArray(['color' => ['$type' => 'color', /* ... */]]);
Other methods: has(string $path): bool, all(): array<string, TokenValueInterface>, and get(string $path, ?string $mode = null). Unknown paths throw a TokenException. Per-token metadata is available via metadata(string $path): ?TokenMetadata and allMetadata() (carrying $description and $deprecated).
Supported token types
The parser handles the following DTCG $type values:
| Type | Value object | Notes |
|---|---|---|
color |
ColorValue |
hex + any CSS Color 4 space, stored losslessly |
dimension |
DimensionValue |
{ value, unit }, unit px / rem / em |
duration |
DimensionValue |
{ value, unit }, unit ms / s |
number |
NumberValue |
|
fontFamily |
FontFamilyValue |
string or list of strings |
fontWeight |
NumberValue |
keyword (bold, medium, …) mapped to numeric |
cubicBezier |
CubicBezierValue |
array of 4 numbers |
boolean |
BooleanValue |
|
string |
StringValue |
|
link |
LinkValue |
|
strokeStyle |
StrokeStyleValue |
keyword or { dashArray, lineCap } |
border |
BorderValue |
composite: color + width + style |
shadow |
ShadowValue |
single or multi-layer, inset supported |
gradient |
GradientValue |
list of { color, position } stops |
transition |
TransitionValue |
duration + delay + timing function |
typography |
TypographyValue |
composite font shorthand; extra props preserved |
Color spaces
ColorValue accepts hex strings (#rrggbb and #rrggbbaa) and DTCG color objects. Colors are stored losslessly in their authored color space — the components (with null representing a CSS none / powerless channel) and alpha are kept verbatim, never squashed to sRGB at parse time.
The accepted color spaces match the CSS Color 4 / terrazzo set: srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, lab, lab-d65, lch, oklab, oklch, okhsv, hsl, hwb, xyz, xyz-d50, xyz-d65. An unknown space throws.
fontWeight keywords (thin, light, regular, medium, semi-bold, bold, black, …) are mapped to their numeric equivalents; an unknown keyword, or a numeric weight outside 1–1000, throws a TokenException.
CSS serialization (toCss() / (string))
Casting to string emits faithful CSS Color 4 — no gamut conversion:
| Color space(s) | Output |
|---|---|
srgb, hex |
rgb(R G B) / rgb(R G B / A) |
hsl |
hsl(H S% L%) |
hwb |
hwb(H W% B%) |
lab, lab-d65 |
lab(L a b) |
lch |
lch(L C H) |
oklab |
oklab(L a b) |
oklch |
oklch(L C H) |
display-p3, a98-rgb, prophoto-rgb, rec2020, srgb-linear, xyz, xyz-d50, xyz-d65 |
color(<space> c1 c2 c3) |
okhsv |
hex fallback (no CSS function) |
A null component renders as none; alpha below 1 is appended as / A inside the function. okhsv has no CSS function, so toCss() emits the author-provided hex fallback if present, otherwise it throws.
Conversion to sRGB (toHex() / toRgb())
These reduce a color to 8-bit sRGB:
srgb,hsl,oklch(and hex): computed directly.- Any other space: uses the author-provided
hexfallback if present, else throws — wide-gamut values are never silently approximated.
For example, display-p3 [1 0 0] serializes to color(display-p3 1 0 0); toHex() throws unless the token also carries a "hex" fallback (e.g. "#fd0000"), in which case that value is returned verbatim.
Value objects
Every token resolves to an immutable value object implementing TokenValueInterface extends \Stringable. Casting to string produces a CSS-ready representation, so value objects can be dropped straight into templates or stylesheets.
Color tokens additionally expose toHex() and toRgb(?float $alpha = null):
$color = $tokens->get('color.primary'); (string) $color; // "rgb(255 0 0)" — default string form is rgb() $color->toHex(); // "#ff0000" $color->toRgb(0.5); // "rgb(255 0 0 / 0.5)"
Theming / modes
Mode-specific values are declared under $extensions.mode.<name>:
{
"color": {
"$type": "color",
"fg": {
"$value": "#ffffff",
"$extensions": { "mode": { "dark": "#000000" } }
}
}
}
Resolve a mode by passing it to get(), or get a mode-bound value object via forMode():
$tokens->get('color.fg'); // base -> rgb(255 255 255) $tokens->get('color.fg', 'dark'); // dark -> rgb(0 0 0) $tokens->get('color.fg')->forMode('dark'); // same as above $tokens->get('color.fg')->forMode('nope'); // falls back to the base value
An unknown mode falls back to the base value rather than throwing.
Modes work for every standard DTCG token type — color, dimension, number, fontFamily, fontWeight, duration, cubicBezier, strokeStyle, border, transition, shadow, gradient, and typography. (The non-spec boolean, string, and link extras are not mode-aware and always return their base value.)
Aliases and modes
Aliases are resolved per mode: an alias resolves to its target's value in the same mode, falling back to the target's base when the target does not declare that mode. A token that aliases a themed token but declares no modes of its own hoists the target's modes:
{
"color": {
"$type": "color",
"blue": { "$value": "#0000ff", "$extensions": { "mode": { "dark": "#000088" } } },
"accent": { "$value": "{color.blue}" }
}
}
$tokens->get('color.accent'); // -> rgb(0 0 255) (blue, base) $tokens->get('color.accent', 'dark'); // -> rgb(0 0 136) (blue, dark) — hoisted through the alias
Hoisting also works through composites (e.g. a border whose color aliases a themed color becomes mode-aware on that channel) and transitively along alias chains. When a token declares its own modes, those take precedence and no extra modes are hoisted. This mirrors Terrazzo's mode resolution.
Twig
The TokenExtension exposes a token() function plus hex and rgb filters.
In a Symfony app it is registered automatically (see Symfony). For plain Twig, wire it as an attribute extension with a runtime loader:
use n5s\DtcgTokens\Tokens; use n5s\DtcgTokens\Twig\TokenExtension; use Twig\Environment; use Twig\Extension\AttributeExtension; use Twig\RuntimeLoader\FactoryRuntimeLoader; $tokens = Tokens::fromFile('tokens.json'); $twig = new Environment($loader); $twig->addExtension(new AttributeExtension(TokenExtension::class)); $twig->addRuntimeLoader(new FactoryRuntimeLoader([ TokenExtension::class => static fn (): TokenExtension => new TokenExtension($tokens), ]));
Then in templates:
{{ token('color.primary')|hex }} {# #ff0000 #}
{{ token('color.primary')|rgb }} {# rgb(255 0 0) #}
{{ token('color.primary')|rgb(0.5) }} {# rgb(255 0 0 / 0.5) #}
{{ token('space.md') }} {# 16px — Stringable, no filter needed #}
{{ token('color.fg', 'dark')|hex }} {# mode-aware lookup #}
The hex and rgb filters only accept color tokens and throw a LogicException otherwise.
Symfony
Enable the bundle:
// config/bundles.php return [ // ... n5s\DtcgTokens\Bridge\Symfony\DtcgTokensBundle::class => ['all' => true], ];
Configure it under the dtcg_tokens key:
# config/packages/dtcg_tokens.yaml dtcg_tokens: files: - '%kernel.project_dir%/assets/tokens/tokens.json' cache: cache.app # optional PSR-6 pool service id
files is required (at least one entry); multiple files are merged in order. cache is optional — point it at any PSR-6 cache pool service to cache the parsed token tree.
Then inject n5s\DtcgTokens\Tokens into any service or controller:
use n5s\DtcgTokens\Tokens; final class ThemeController { public function __construct(private Tokens $tokens) {} }
The Twig token() function and hex / rgb filters are registered automatically when Twig is installed.
CSS export
CssExporter renders a Tokens collection as CSS custom properties:
use n5s\DtcgTokens\Export\CssExporter; echo new CssExporter()->export($tokens);
:root { --color-primary: rgb(255 0 0); --space-md: 16px; }
The constructor takes an optional prefix and selector:
new CssExporter(prefix: 'ds', selector: '.theme-dark')->export($tokens); // .theme-dark { --ds-color-primary: rgb(0 0 0); ... }
Token paths are slugged by replacing . and spaces with -; values use each value object's string form.
Caching
CachedTokenFactory wraps a loader and an optional PSR-6 pool. It parses tokens once, caches the result, and — in debug mode — invalidates the cache when any source file's mtime changes:
use n5s\DtcgTokens\Cache\CachedTokenFactory; use n5s\DtcgTokens\Loader\JsonFileLoader; $factory = new CachedTokenFactory( loader: JsonFileLoader::fromPaths(['tokens.json']), cache: $psr6Pool, // any Psr\Cache\CacheItemPoolInterface, or null debug: $isDebug, ); $tokens = $factory->create();
Without a cache pool it simply parses on first create() and reuses the result in-process. The Symfony bundle wires this factory for you.
Development
composer qa # PHPStan (max), ECS, Rector, PHPUnit
Built and tested against PHP 8.4. PHPStan runs at max level with strict rules; ECS and Rector enforce style and modernization.
Limitations
This is a runtime library. It deliberately leaves some things out:
| Not included | Notes |
|---|---|
| Build-time codegen / plugins | Runtime only — for a full token toolchain with codegen and plugins, see Terrazzo |
| Resolver-document format | |
| Alias-graph metadata | Aliases are resolved in place (including per mode); the reference graph itself is not exposed as data |
| Gamut-conversion math | Colors serialize faithfully to CSS Color 4; reducing a wide-gamut space to sRGB (toHex() / toRgb()) needs an sRGB-reducible space (srgb, hsl, oklch) or an author-provided hex fallback |
Credits
Feature scope was informed by Terrazzo, the state-of-the-art JavaScript reference for DTCG tooling.
License
MIT.