Skip to content

Review brief: Design Tokens API and UI Suite

Author: Shibin Das (d34dman)Last updated

In review

Reviewers: open, join #3627977 on drupal.org

Try it yourself

Everything in this brief runs on the demo site: Drupal 11.5.0-beta1 with the module, its adapters, the Media Library pilot and demo content. You need DDEV 1.25 or newer:

git clone https://github.com/d34dman/design_system_api_demo.git
cd design_system_api_demo
ddev start && ddev install-demo   # prints a one-time login link

Why this reaches your area

The module is built on core's Design Tokens API (#3531854), as first shipped in Drupal 12.0.0-beta1 and 11.5.0-beta1. It doesn't have its own token loader: the shared vocabulary is a {provider}.tokens.yml file in DTCG format, discovered by plugin.manager.design_token. On top of it, the module adds what it needed and core doesn't have yet: values per context, references, a layered resolver, and interim color and shadow value types until #3622909 adds the remaining DTCG types to core. Some of that probably belongs in core, and some of it may clash with where the API is going. That's what we'd like to find out before an alpha.

Questions for you

  1. Value types and the values adapters need. Core 12.0.0-beta1 has dimension, fontFamily, fontWeight, number and typography; #3622909 adds the rest of DTCG's types, starting with color, border and shadow (work in progress by this project's maintainer). Core's types take strict DTCG values (a dimension is {value, unit}), but an adapter needs var(--admin-color-bg) as a value and an author needs {color.action.primary}. Should core's value types accept references and var(), or should those be resolved before a value reaches the plugin?
  2. References (#3590100). We need a reference such as {color.action.primary} to stay a reference until a value is resolved for a context, because its target has different values in light, dark and high contrast. The comments on #3590100 lean toward resolving at plugin discovery. Would keeping references until render time work for core? → §4 step 3, Q15
  3. $extensions. Core drops $extensions from definitions, so we parse the files a second time to read our per-context defaults ($extensions.design_system_api: { mode=dark: …, contrast=more: … }). Could definitions keep $extensions, or is there a better place for context values?
  4. Overrides per selector vs. contexts. Core's design_token config entity stores overrides per CSS selector. We keep author values per theme, with page-side contexts (dark, more contrast) delivered as selector-scoped blocks. Should author values become design_token entities, with the theme as the selector scope? → Q16, §9
  5. Who owns a shared vocabulary. Themes choose their own token names today. This project proposes semantic names that every theme fills and contrib codes against. Should those names live in this contrib project, in UI Suite, or eventually in core? → Q4, Q5

What we built

  • Vocabulary: design_system_api.tokens.yml, 43 semantic tokens (color.bg.*, color.fg.*, color.border.*, color.action.*, color.status.*, color.focus, space.*, radius.*, font.size.*, font.weight.*, shadow.*, focus.ring.*). $value is the frontend default. $extensions.design_system_api holds values for mode=dark, contrast=more, both together (mode=dark contrast=more) and the admin role.
  • Value types (interim): color and shadow plugins accept a CSS value or a {reference}, and reject anything that could break out of a declaration (;, {, }, <, >, \, /*). For an invalid value they throw a PluginException; core drops such a token, and drush ds:tokens reports it.
  • References: only color tokens use references in v0 (the shadow plugin accepts them, but no shadow uses one). A reference as the value of a token with a core type (for example a dimension) is dropped and reported: core's value plugin throws a TypeError (ValueError for fontWeight), which core doesn't catch, so the module replaces core's value plugin manager with a subclass that turns these into a PluginException. The plugin IDs are the ones core will use. Once #3622909 lands, the module drops its own plugins, and the vocabulary has to move to core's value shapes too: colors are hex strings and shadows CSS strings today.
  • Resolver (src/Resolver/): layers in order author → adapters → contract defaults. The highest layer that sets a token owns it in every context. References are resolved per context, with cycle detection, and every value records where it came from. Only a whole-value reference ({color.focus}) is checked against the token's type; one inside a value (0 0 0 2px {color.focus}) isn't. This is the module's own layer list, not the DTCG Resolver format that §9 names.
  • Output: one CSS file per theme with --ds-* custom properties, plus blocks for dark mode, more contrast and both together that hold only the values that differ. Each kind of context has its own sub-layer, in the order base, contrast, mode, mode-contrast.

Component tokens use the same file format. The Media Library pilot declares media-card.border with $value: '{color.border.default}': the reference is the suggested mapping, and a site's author can override it.

See it

ddev drush ds:tokens --theme=olivero
ddev drush ds:resolve media-card.border --theme=olivero --mode=dark
--ds-media-card-border: #3f3f46
theme=olivero (frontend), context=mode=dark
• media-card.border = {color.border.default}   ← default:frontend [base]
  └ color.border.default = #3f3f46   ← default:frontend [mode=dark]

Known issues

Found in our own check of the code. Items marked Fixed name the commit that fixed them. We track the open ones ourselves; if one matters for your review, say so in this brief's issue.

  • Fixed (191fe37): a reference in a core-typed token (dimension, number, fontWeight) is now dropped and reported (drush ds:tokens) instead of breaking token discovery for the whole site. Core issue: #3628095.
  • Embedded references aren't type-checked, and the shadow type has no type check, only the check for characters that break out of a declaration.
  • Partly fixed (bb2a125): a combined context mode=dark contrast=more, in a last sub-layer, now covers dark and more contrast on one element, nested either way, and OS plus page choices; kernel and browser tests check it and every text and UI contrast pair; since 77237c5 a [data-ds-mode="light"] region on an OS-dark page with OS more contrast keeps the contrast values. Still open: contract contrast values don't apply to the tokens an adapter maps, which keep the theme's own values in every context.
  • Partly fixed (b79f487): cron now deletes old generated CSS files once they are older than core's stale_file_threshold. Still open: the token CSS isn't a library; it is a html_head_link on every page, written to public:// during a page request.
  • A changed adapter file isn't part of the CSS cache key, so it shows only after a cache rebuild.
  • Author values are files outside config, so config sync and recipes don't carry them.
  • Fixed (b8ffdc0): the Media Library pilot, which removes the CSS of theme libraries that extend Media Library, now sets pointer-events: none on disabled items itself, so the selection limit holds; FunctionalJavascript tests cover it.
  • §4 and §9 describe the target: v0 has no @scope and doesn't use core's design_token overrides.

Read only these sections

What we heard

Nothing yet.


Edit note (2026-10-03): Corrected the token count, the claim that a reference in a core-typed token is dropped (it throws), which references are type-checked, that no shadow uses a reference, and that dropping the interim plugins also needs a vocabulary migration; noted that the resolver isn't the DTCG Resolver format. Added Known issues. All found in a pre-review check of the code.

Edit note (2026-10-03, later): Updated What we built and Known issues for the fixes in 191fe37 (references in core-typed tokens are dropped), bb2a125 (combined dark + more contrast context and its sub-layer), b79f487 (old CSS files are cleaned up) and b8ffdc0 (selection limit in the pilot). The core issue is now linked (#3628095). Open known issues are now tracked by the maintainers instead of as separate issues.

Edit note (2026-10-18): Updated Known issues for 77237c5 (light region keeps more contrast); the API also ships in 11.5.0-beta1, and the demo site now runs Drupal 11.5.0-beta1 (the module supports ^11.5 || ^12).