fylgja / props-builder
Effortlessly generate Design Tokens (CSS custom properties) from PHP arrays or JSON.
Requires
- php: >=8.1
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-16 07:47:24 UTC
README
A PHP port of @fylgja/props-builder. It converts PHP arrays or JSON files into CSS custom properties, SCSS variables or Design Token files, so the same tokens can be built from backend logic without Node.
The output matches the JavaScript version, so both can be used side by side.
Installation
composer require fylgja/props-builder
Requires PHP 8.1 or newer.
Usage
use Fylgja\PropsBuilder\PropsBuilder; PropsBuilder::build( [ 'color' => [ 'red' => '#f00', 'green' => '#0f0', 'blue' => '#00f', ], ], 'output.css', );
This writes output.css with the following content:
:where(:root) { --color-red: #f00; --color-green: #0f0; --color-blue: #00f; }
build() always returns the generated output, so it can also be used without writing a file.
Direct output
Set writeToFile: false to only get the output as a string, for example to render the tokens in a <style> tag.
The filename is still required, since it picks the output format, but nothing is written to it.
<?php use Fylgja\PropsBuilder\PropsBuilder; $css = PropsBuilder::build( [ 'color' => [ 'primary' => '#1d4ed8', 'surface' => '#ffffff', ], 'radius' => '0.5rem', ], 'tokens.css', writeToFile: false, ); ?> <style><?= $css ?></style>
This renders:
<style>:where(:root) { --color-primary: #1d4ed8; --color-surface: #ffffff; --radius: 0.5rem; } </style>
Instead of a filename with the right extension, you can also set the format with parseAs, e.g. PropsBuilder::build($props, 'inline', parseAs: 'css', writeToFile: false).
To scope the tokens to a part of the page, such as a theme picked in the backend, set the selector:
$css = PropsBuilder::build( $brandTokens, 'tokens.css', writeToFile: false, selector: '[data-theme="brand"]', );
Every output format works this way.
For example, parseAs: 'json' or parseAs: 'css-jit' gives the tokens as JSON, to pass them to JavaScript in a <script type="application/json"> tag.
Important
The output is not escaped for HTML.
If the token values come from user input, such as colors set in an admin panel, a value containing </style> can break out of the tag.
Validate those values, or escape the < character before printing the output:
str_replace('<', '\3C ', $css) for CSS, and str_replace('<', '\u003c', $json) for JSON.
Use case: a theme set in the backend
A common case is letting store owners or editors pick the brand colors in an admin panel. The default tokens stay in your stylesheet, and only the brand values are built per request and printed in the page head.
<?php use Fylgja\PropsBuilder\PropsBuilder; $settings = loadThemeSettings(); // e.g. from the database or a config file $theme = [ 'color' => [ 'primary' => $settings['primary'], 'surface' => $settings['surface'], ], 'color-@media:dark' => [ 'primary' => $settings['primary_dark'], 'surface' => $settings['surface_dark'], ], ]; $css = PropsBuilder::build($theme, 'theme.css', writeToFile: false); ?> <head> <link rel="stylesheet" href="/css/app.css"> <style><?= str_replace('<', '\3C ', $css) ?></style> </head>
The -@media:dark group becomes a dark mode block with the same custom property names:
:where(:root) { --color-primary: #1d4ed8; --color-surface: #ffffff; } @media (prefers-color-scheme: dark) { :where(:root) { --color-primary: #93c5fd; --color-surface: #0f172a; } }
Print the <style> tag after the stylesheet. Both use the same selector, so the one that comes last wins.
When the settings rarely change, you can also build a file once when they are saved, instead of on every request. The browser can then cache it like any other stylesheet:
PropsBuilder::build($theme, __DIR__ . '/public/css/theme.css');
Options
The options are named arguments, the same as the options object in the JavaScript version.
PropsBuilder::build( $props, // Required: array, object or (for Stitch input) a Markdown string. $filename, // Required: name of the output file, also picks the output format. parseAs: 'auto', // Output format, defaults to the format of the filename. writeToFile: true, // Write the output to $filename. selector: ':where(:root)', // CSS selector for the custom properties (css output only). wrapper: '', // Nests the Design Tokens under this key (e.g. for Figma). name: 'Design Tokens', // Name of the Design Tokens Resolver (dtcg output only). description: '', // Description of the Design Tokens Resolver (dtcg output only). banner: '', // Prepended to the output as is. inputTypeTokens: false, // Set to true if $props is a Design Tokens file. inputTypeSyntax: 'default', // Syntax of the input tokens: default, figma, style-dictionary or stitch. stripPrefix: '', // Dot/dash separated key path (or list of keys) to unwrap (e.g. 'tokens.values'). rename: null, // Map of [currentKey => newKey], applied recursively (e.g. ['colors' => 'color']). onWarning: null, // Receives warnings, such as skipped dtcg tokens. Defaults to error_log(). );
Options can also be passed from an array with PropsBuilder::build($props, $filename, ...$options).
Lists and scales
PHP arrays are both lists and maps, so a list is treated as a value list, the same as a JSON array. A value list is joined with a comma, e.g. the layers of a shadow or a font stack.
'shadow' => [ 1 => ['0 1px 2px #0002', '0 2px 4px #0002'], // --shadow-1: 0 1px 2px #0002, 0 2px 4px #0002; ],
A scale keyed from 0 is also a list in PHP. Pass it as an object to keep it a scale:
'gray' => (object) ['#f8f9fa', '#f1f3f5', '#e9ecef'], // --gray-0, --gray-1, --gray-2
For JSON files, decode them as objects (the default of json_decode()), so every JSON object stays a map:
$tokens = json_decode(file_get_contents('tokens.json'));
Output formats
The output format is picked from the filename, unless parseAs is set.
| Filename | parseAs |
Output |
|---|---|---|
*.css |
css |
CSS custom properties |
*.scss |
scss |
SCSS variables |
*.js, *.mjs, *.cjs, *.json |
mjs, cjs, json |
The props as is |
*.resolver.json |
dtcg |
Design Tokens Resolver (2025.10) |
*tokens.json |
tokens |
Design Tokens (legacy draft syntax) |
*figma-tokens.json |
figma |
Figma Tokens |
*style-dictionary-tokens.json |
style-dictionary |
Style Dictionary tokens |
css-jit |
A flat object for JIT tools, like jit-props |
To Design Tokens 2025.10
The dtcg output follows the Design Tokens 2025.10 specification.
Values are converted to typed token values, e.g. 0.5rem becomes a dimension token with { "value": 0.5, "unit": "rem" },
and var(--shadow-color) becomes a {shadow-color.$root} reference.
PropsBuilder::build($props, 'tokens/fylgja.resolver.json', name: 'Fylgja');
All tokens go in a single set of a Resolver document. Dark mode values (@media:dark) are left out.
The specification has no way to express some CSS values, such as calc(), clamp(), var() inside a color, or units other than px and rem (or ms and s for durations).
These tokens are skipped with a warning, together with any token referencing them.
The warning goes to error_log(), pass onWarning to handle it yourself.
From Design Tokens to CSS
use Fylgja\PropsBuilder\PropsBuilder; $tokens = json_decode(file_get_contents('path/to/your/tokens.json')); PropsBuilder::build($tokens, 'tokens.css', inputTypeTokens: true);
Configure the input based on the format of your file:
- A simple key value file: no options needed.
- A W3C Design Tokens file (including 2025.10 and Resolver files): set
inputTypeTokens: true. Design Tokens 2025.10 values, such as color objects or dimensions, are converted to CSS values. Composite tokens become CSS shorthands (border,shadow,transition, and gradient color stops), except fortypography, which becomes a custom property per part (e.g.--heading-font-size). Groups support$typeinheritance,$roottokens, and$extends. Both{token.path}references and$refJSON Pointers within the same file are supported. For a Resolver file, all sets are merged in the resolution order, and each modifier uses itsdefaultcontext. A$refto another file is not supported, merge those files into a single document first. - A Figma Tokens file: set
inputTypeTokens: trueandinputTypeSyntax: 'figma'. - A Google Stitch Markdown export: set
inputTypeTokens: trueandinputTypeSyntax: 'stitch', and pass the raw Markdown content (a string) as$props. Only the YAML frontmatter is used, and every key in it is converted to tokens, except for known metadata keys (name,description). Thecolorsgroup is renamed to the singularcolor(unless acolorgroup already exists).
Differences from the JavaScript version
- JavaScript sorts integer keys of an object before the other keys, PHP keeps the order they were written in. The output is the same, only the order of some properties can differ.
- The
style-dictionaryinput syntax does not log a hint to use the Style Dictionary converter.
Development
composer install
composer test
composer parity
composer parity runs the cases in tests/parity through both this package and the Node version, and fails on any difference in the output.
It expects a checkout of fylgja/fylgja next to this repo, or a path to its props-builder directory in PROPS_BUILDER_JS.
See AGENTS.md for how to keep this package in sync with the Node version.