Skip to content

[DesignTokens] Add the Design Tokens component - #3932

Open
smnandre wants to merge 4 commits into
symfony:3.xfrom
smnandre:sa/ux-design-tokens
Open

smnandre wants to merge 4 commits into
symfony:3.xfrom
smnandre:sa/ux-design-tokens

Conversation

@smnandre

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? yes
Deprecations? no
Documentation? yes
Issues
License MIT

Symfony UX Design Tokens reads design tokens written in the DTCG 2025.10 format (W3C Design Tokens Community Group) and hands their resolved values to CSS, Twig and PHP.

# config/packages/ux_design_tokens.yaml
ux_design_tokens:
    resolver:
        path: '%kernel.project_dir%/design/theme.resolver.json'
{{ ux_token_css() }}
<h1 style="color: {{ ux_token('color.action.primary') }}">Welcome</h1>
$tokens->get('color.action.primary', ['brand' => 'sky']); // typed ColorToken

What it provides:

  • DTCG Format, Color and Resolver modules: the 13 token types, aliases, $ref JSON Pointers, $extends, sets and modifiers
  • CSS custom properties with @property rules; light and dark written from a Resolver modifier, the dark block holding only what changes
  • ux_token(), ux_token_css() and ux_token_stylesheet() (a versioned AssetMapper file)
  • TokenRegistryInterface: typed token values in PHP, with Resolver inputs per call
  • lint:design-tokens (with --fix), debug:design-tokens --sources, ux:design-tokens:export (CSS, JavaScript, DTCG, Tailwind CSS v4, DESIGN.md) and ux:design-tokens:import (Tailwind CSS v4)
  • Resolutions cached in the system cache, the stylesheet written at warmup

I tried here to keep the minimum of things for ease review and first iterations. As I see it, the next steps could be:

  • CSS mapping profiles (shadcn, easyadmin, flowbites, material, bootstrap..)
  • Adapters/bridges for Notifier / Mailer / TUI
  • Figma bridge

Website and demo in the making, not sure they'll be ready before next week

Parse and validate DTCG 2025.10 token documents (Format and Color
modules), resolve aliases, $ref JSON Pointers, $extends and type
inheritance, and assemble themes from Resolver documents: sets,
modifiers, contexts and resolutionOrder.

TokenRegistry exposes the resolved tokens by path, for any set of
Resolver inputs.
Render the resolved tokens as CSS custom properties, inline with
ux_token_css() or as an AssetMapper stylesheet with
ux_token_stylesheet(). The color_scheme option maps a Resolver
modifier to prefers-color-scheme and [data-theme]; the dark block
holds only the variables that change.

Resolutions are cached in the system cache and warmed with the
application. Add lint:design-tokens, debug:design-tokens and
ux:design-tokens:export (DTCG, CSS, JavaScript).
ux:design-tokens:import converts a Tailwind CSS v4 @theme block to
DTCG. Values DTCG cannot hold are logged as warnings, entries it has
no type for are listed with -v.

ux:design-tokens:export gains the "tailwind" and "design.md" formats;
design.md needs symfony/yaml. Any ImporterInterface service joins the
import command through autoconfiguration.
Add the README, the CHANGELOG and a single documentation page:
writing tokens, configuration, CSS output with light and dark,
reading tokens in Twig and PHP, Resolver themes, export and import,
and the lint and debug commands.
DocumentationExamplesTest checks that each documented example parses.
@carsonbot carsonbot added Documentation Improvements or additions to documentation Feature New Feature Status: Needs Review Needs to be reviewed labels Sep 27, 2026
Comment on lines +207 to +213
/* assets/styles/app.css */
.button {
background: var(--dt-color-action-primary);
color: var(--dt-color-content-default);
border-radius: var(--dt-dimension-radius-control);
padding: var(--dt-dimension-spacing-md);
}

@Kocal Kocal Sep 27, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many questions here:

  • Where does this file comes from?
  • Is it automatically generated from somewhere, or is something you write manually?
  • Is VSCode or PHPStorm able to autocomplete --dt- variables? If no, I'm afraid the DX is bad :/

At section Reading a token, you show <h1 style="color: {{ ux_token('color.action.primary') }}">Welcome</h1>, where you read a token with ux_token() and directly inject it in style:

  • is it the good way to go, to keep styles around its HTML ?
  • how do you deal with hover: or ::before?
  • do you have auto-completion / validation on the value passed to ux_token()?

@Kocal Kocal Sep 27, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think you must insist on answering these questions on the documentation, with a lot of examples 🙏🏻

EDIT: From a purely pragmatic standpoint, I think #3933 will solve this issue. For now, the PR reads its tokens from its ux_css.yaml, but I'm sure it will be able to read the tokens from DesignTokens instead :)

Comment on lines +285 to +287
``ux_token_css()`` writes the CSS into every response. When the stylesheet is
the same for every visitor, ``ux_token_stylesheet()`` links it instead, so the
browser caches it. It needs AssetMapper:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really needs to depends on AssetMapper to include a CSS file on a page? 😕

In #3933, a .css file is automatically generated, meaning that you can easily load it through AssetMapper, or load it through a Bundler.

Comment on lines +47 to +63
public function renderStylesheet(): string
{
if (null === $this->assetMapper) {
throw new LogicException('Serving design tokens as a stylesheet requires the AssetMapper component. Try running "composer require symfony/asset-mapper", or use ux_token_css() to inline the CSS instead.');
}

$this->stylesheets?->path();

$logicalPath = UXDesignTokensBundle::ASSET_NAMESPACE.'/'.StylesheetCache::STYLESHEET;
$publicPath = $this->assetMapper->getPublicPath($logicalPath);

if (null === $publicPath) {
throw new RuntimeException(\sprintf('No design token stylesheet was found at "%s".', $logicalPath));
}

return \sprintf('<link rel="stylesheet" href="%s">', htmlspecialchars($publicPath, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I misunderstand something here, but how users using bundlers are supposed to include the .css file (with the tokens) on their page?

Kocal added a commit to Kocal/symfony-ux that referenced this pull request Sep 27, 2026
UX CSS now reads every design token from symfony/ux-design-tokens (symfony#3932): DTCG token files, resolved when the container is built, with every Resolver context included. A token's name in css() comes from its path, with a fixed prefix per category, for example color.* for colors and dimension.spacing.* for spacing. Generated rules use UX Design Tokens' own CSS variables, like var(--dt-color-primary), so values, dark mode and per-request themes come from there. _dark reuses the same selectors as its stylesheet.

The tokens, semantic_tokens, default_tokens and breakpoints options of ux_css are removed; breakpoints now come from breakpoint.* tokens. CI stays red until symfony#3932 is merged, since the package isn't in the repository yet.
Kocal added a commit to Kocal/symfony-ux that referenced this pull request Oct 1, 2026
| Q              | A
| -------------- | ---
| Bug fix?       | no
| New feature?   | yes
| Deprecations?  | no
| Documentation? | yes
| Issues         |
| License        | MIT

This PR adds a new package, `symfony/ux-css`. It gives Twig a `css()` function in the spirit of Panda CSS (https://panda-css.com/): you pass a hash of style properties and get back atomic class names built from design tokens. The hash is checked when Twig compiles the template, so a misspelled property or an unknown token shows up as a Twig syntax error pointing to the template line, not as a silent CSS bug.

PHP writes one CSS file (`var/ux_css/styles.css`). There is no Node.js involved. AssetMapper serves the file directly, and Webpack Encore or Symfony Reprise can import it like any other stylesheet. Class names and generated CSS match what Panda CSS produces (a pinned version) for the supported syntax, checked against test cases recorded from Panda's own test suite.

```html+twig
<div class="{{ css({ p: 'md', color: 'fg', _hover: { color: 'primary' }, md: { p: 'lg' } }) }}">
```

This renders `class="p_md c_fg hover:c_primary md:p_lg"`.

This is complementary to symfony#3932 (Simon's "[DesignTokens] Add the Design Tokens component"), not competing with it. symfony#3932 reads DTCG token files and exposes their values as CSS variables and PHP values. This PR is a styling API that consumes tokens. A later step could let `ux-css` read its tokens from `ux-design-tokens` instead of its own config. I open this as a draft so both approaches can be compared side by side.

Most of the diff, about 64,000 of the 114,000 lines, is test fixtures recorded from Panda CSS: reviewers can skip `src/Css/tests/Fixtures/Panda/`. The package suite passes (5445 tests, 224 skipped because they cover Panda syntax we don't support), and PHP-CS-Fixer, Twig-CS-Fixer and DOCtor-RST all pass too.
Kocal added a commit to Kocal/symfony-ux that referenced this pull request Oct 1, 2026
UX CSS now reads every design token from symfony/ux-design-tokens (symfony#3932): DTCG token files, resolved when the container is built, with every Resolver context included. A token's name in css() comes from its path, with a fixed prefix per category, for example color.* for colors and dimension.spacing.* for spacing. Generated rules use UX Design Tokens' own CSS variables, like var(--dt-color-primary), so values, dark mode and per-request themes come from there. _dark reuses the same selectors as its stylesheet.

The tokens, semantic_tokens, default_tokens and breakpoints options of ux_css are removed; breakpoints now come from breakpoint.* tokens. CI stays red until symfony#3932 is merged, since the package isn't in the repository yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation Feature New Feature Status: Needs Review Needs to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants