Symfony Twig theme (twig.default_path, FrameworkBundle template paths) is unrelated to UI Kernel themes (data-theme, ThemeRegistry). See Symfony Twig theming.
Built-in themes ship as W3C DTCG token files — not bespoke symfinity_ui_kernel.themes YAML:
config/
├── design-systems/symfinity.dtcg.yaml # platform semantic vocabulary (eight colours, schema 2.0)
└── themes/{lineage}/
├── theme.meta.yaml # palette recipe + variant registry
├── {variant}.dtcg.yaml # theme-layer semantic + appearance tokens
└── …
| File | Role |
|---|---|
theme.meta.yaml |
lineage, optional design_system_id, shared palette, variants[] with layer_file, tone, mode |
{variant}.dtcg.yaml |
theme layer — semantic colour aliases (color.primary → {color.blue.600}), spacing, radius, motion |
design-systems/{id}.dtcg.yaml |
design_system layer — cross-theme vocabulary extensions |
Each lineage SHOULD set design_system_id: symfinity in theme.meta.yaml (default when omitted). The registry loads config/design-systems/{id}.dtcg.yaml. Unknown ids fail at stack-build time with UnknownDesignSystemException.
Runtime resolution: base (OKLCH palette DTCG from generator) ⊕ design_system ⊕ theme → LayeredTokenResolver → --ui-* CSS.
Authoring consumer themes (export YAML using the bespoke preset / tone / semantics schema via AuthoringThemeConfig) do not register in config/themes/. See DTCG token core and the authoring boundary below.
Flex apps may ship DTCG lineages under config/themes/{lineage}/ (same layout as bundle). App lineages override bundle lineages with the same folder name; invalid app lineages are skipped with a warning.
# config/packages/symfinity_ui_kernel.yaml
symfinity_ui_kernel:
themes_directory: '%kernel.project_dir%/config/themes'Merged variants appear in ThemeRegistry when app lineages are present.
| Id | Label | Notes |
|---|---|---|
default |
Balanced | Light neutral baseline |
default-dark |
Balanced dark | Dark neutral baseline |
semantic |
Semantic | Bootstrap-inspired semantic colours, roomy spacing |
semantic-dark |
Semantic dark | Dark semantic palette |
utility |
Utility | Tailwind-inspired slate/blue palette, compact spacing |
utility-dark |
Utility dark | Dark utility palette |
Set the active lineage in config:
symfinity_ui_kernel:
default_theme: semantic
default_variant: semantic
default_physics: flatMaterial character — radius, elevation shadow model, motion durations/easing, hover lift — is orthogonal to palette, mode, and Preset (typography + spacing). Three profiles ship in kernel CSS:
| Physics | DOM value | Notes |
|---|---|---|
| flat | flat |
Default; opaque surfaces, quick motion, no hover lift |
| glass | glass |
Dark-mode only (light + glass corrects to flat) |
| retro | retro |
Zero radius, hard offset shadow, stepped/instant motion |
Set on [data-ui-root] (or html):
<html data-ui-root data-theme="semantic-dark" data-ui-physics="glass">Author in theme.meta.yaml:
physics: flat # lineage default; per-variant override in variants[]CssGenerator emits [data-ui-physics="…"] blocks with --ui-physics-* tokens and bridge aliases to --ui-motion-* / --ui-radius-*. Complements data-ui-surface="glass" on components (component blur) — not a merge.
Built-in themes map to one of two rhythm profiles:
| Profile | Spacing | Typography | Lineages |
|---|---|---|---|
| Semantic | Roomy | 1rem base | default, semantic pairs |
| Utility | Compact | 0.875rem base | utility pair |
Radius, shadow, and motion character follow the active physics profile (default flat). DTCG theme layers still define per-lineage appearance tokens until authors opt into non-flat physics via data-ui-physics.
Public 0.3.x line — runtime switching, overlays, and documented capability ids. Theme YAML stays on schema_version: '2.0' until a later release explicitly versions it.
| Theme-engine capability | ui-kernel v0.3.0 | ui-kernel v0.3.1 | symfinity/ui-designer v0.1.0 |
|---|---|---|---|
| Runtime switching (boot script, cookies, scheme API) | v0.3.0 | — | — |
Theme overlays (user_tokens, app DTCG overrides) |
v0.3.0 | — | — |
| Session preview overlay (preview hosts only) | v0.3.0 | — | Studio + session draft |
| Theme capabilities vocabulary (documented ids) | v0.3.0 | — | — |
Runtime capability probe (ui_kernel_theme_capabilities()) |
v0.3.0 | — | — |
Nested themes (extends single-hop merge) |
— | v0.3.1 | Uses kernel extends |
| Theme inheritance (multi-hop chains) | — | v0.3.1 | Uses kernel chains |
| Theme composition (author graphs) | Kernel ports only | — | v0.1.0 |
v0.3.0 ships runtime_switch, user_token_overlay, session_preview_overlay, and app_dtcg_override, plus the runtime probe — see Theme capabilities. v0.3.1 adds functional extends on variant entries and multi-hop theme-layer merge. symfinity/ui-designer v0.1.0 is the dev-only author product (composition graph, import/export, visual studio at /ui-designer) — it requires ui-kernel ^0.3.1 for inheritance; it is not a ui-kernel patch release.
Capability id: runtime_switch — see Theme capabilities and Theme engine milestones.
Include ui_kernel_theme_boot_script() in <head> before theme CSS so document.documentElement.dataset.theme is set before the first paint. Prefer the bundle head partial:
{% include '@UiKernel/_head.html.twig' %}Manual order when not using the partial:
{{ ui_kernel_theme_boot_script() }}
{{ ui_kernel_layer_order()|raw }}
{{ ui_kernel_css()|raw }}The inline script reads preference cookies, resolves light/dark variant ids per lineage, and sets root.dataset.theme plus root.style.colorScheme.
| Cookie | Purpose |
|---|---|
symfinity_ui_kernel_lineage |
Active lineage (default, semantic, utility, …) |
symfinity_ui_kernel_scheme |
auto, light, or dark |
Unknown or removed lineage values fall back to default_theme from config. Invalid scheme values fall back to auto. Cookies are HttpOnly, SameSite=Lax, one-year TTL.
Set cookies via the scheme API, theme shell presenter, or your own controller using the same keys — do not invent parallel preference storage.
When scheme is auto, the boot script may PATCH the kernel scheme endpoint (same route as ui_kernel_theme_shell()) with optional systemPrefersDark from matchMedia('(prefers-color-scheme: dark)') when the Client Hint header is absent. The JSON response carries resolved themeId, colorScheme, and refreshed CSS for #ui-kernel-theme-css.
Without a scheme endpoint, auto resolves client-side from prefers-color-scheme using the configured lineage pair (semantic / semantic-dark, etc.).
Each built-in lineage exposes a light/dark variant pair. With scheme: auto and no stored cookie override, the active variant follows OS dark preference for that lineage.
Markup or query ?theme= with an unknown id throws in PHP paths that validate against ThemeRegistry. The boot script falls back to the config default variant id when lineage resolution fails. After upgrades, stale cookies for removed lineages reset to the configured default lineage.
Shared vocabulary for apps, dogfood labs, and optional packages:
| Capability id | Meaning | Consumer entry point |
|---|---|---|
runtime_switch |
User or OS changes variant without redeploy | Boot script, cookies, scheme API, ui_kernel_theme_shell() |
user_token_overlay |
Config user_tokens merge atop registry theme |
symfinity_ui_kernel.user_tokens in YAML |
session_preview_overlay |
Ephemeral author draft CSS on preview hosts | SessionThemeInjectionPort (ui-designer implements) |
app_dtcg_override |
App config/themes/{lineage}/ overrides bundle |
themes_directory + merged ThemeRegistry |
nested_theme |
Child variant resolves through parent theme layer | extends on variant entry in theme.meta.yaml |
inheritance_chain |
Multi-hop parent merge (depth limit configurable) | Same as nested; probe true when chain length ≥ 2 hops |
Runtime probe: ThemeCapabilityProbe::enabled() and ui_kernel_theme_capabilities() report which capability ids are active in the current app — see reference.md.
Consumer checklist
- Call
ui_kernel_layer_order()before theme CSS (0.2.4+). - Include
ui_kernel_theme_boot_script()in<head>when usingruntime_switch. - Document cookie / scheme behaviour for your shell.
- Use preview injection only on designated preview routes with a valid session token.
Capability ids: nested_theme, inheritance_chain — see Theme capabilities.
Add optional extends on a variant entry to inherit another registered variant's theme-layer DTCG before applying child overrides:
# config/themes/brand-a/theme.meta.yaml
variants:
- id: brand-a
layer_file: brand-a.dtcg.yaml
extends: semantic# config/themes/brand-a/brand-a.dtcg.yaml — theme-layer overrides only
color:
primary:
$type: color
$value: '{color.red.600}'Rules:
extendsreferences a variant id registered in the merged catalog (built-in or app), not a lineage folder name alone.- Merge walks the chain root → leaf; each step replaces whole tokens by path (same semantics as layer stack merge).
- Cycles and chains longer than
symfinity_ui_kernel.theme_inheritance.max_depthfail at catalog load. - Child themes without overrides inherit the parent theme layer output unchanged.
1. Resolve `extends` chain → merged theme-role DTCG (when present)
2. Built-in or app registry theme (DTCG stack: base ⊕ design_system ⊕ merged theme)
3. Config user_tokens overrides on the active variant
4. Generated --ui-* CSS in @layer ui-kernel-theme
1. Registry baseline (built-in ids unchanged)
2. Injected preview CSS from SessionThemeInjectionPort (separate stylesheet or inline block)
3. MUST NOT mutate ThemeRegistry entries
When both user_tokens and preview injection apply, preview hosts use the session overlay on top of the registry baseline; config user_tokens still apply to generated baseline CSS before preview CSS is injected.
Pair light/dark ids per lineage (default / default-dark, etc.). The boot script and scheme API above are the supported switching path — see Runtime theme switching.
CssGenerator emits:
- Theme tokens (
--ui-color-*,--ui-space-*,--ui-radius-*, …) - Physics blocks (
[data-ui-physics="flat|glass|retro"]with--ui-physics-*+ bridged motion/radius) - Overlay tokens (
--ui-overlay-surface,--ui-overlay-border,--ui-overlay-shadow,--ui-backdrop-color,--ui-backdrop-blur) - Structural profile globals (breakpoints, z-index ladder, layout roles such as grid/stack)
Mono-based semantic colour refs follow the active theme tone (warm/cool/pure families) so text and surface roles stay coherent within a lineage.
It does not emit [data-ui-role] component rules. Install symfinity/ux-blocks-* tier packages for component CSS.
Consumer themes authored with the export YAML pipeline use AuthoringThemeConfig + ThemeTokenResolver — not the built-in DTCG catalog. Authoring packs live outside config/themes/ and resolve through the authoring pipeline, not BuiltinDtcgThemeCatalog.
Override public --ui-* keys at deploy time without forking theme YAML:
symfinity_ui_kernel:
default_theme: semantic
user_tokens:
'--ui-color-primary': '#1a4fd6'
'--ui-color-secondary': '#6c757d'Unknown keys are rejected at merge time.
Adjust breakpoints or container widths via config (not user_tokens):
symfinity_ui_kernel:
system_profile:
breakpoints:
md: 800When caching generated CSS, include profile identity in your cache key alongside theme id and user token hash.
Built-in themes use system font stacks for --ui-font-family-sans and --ui-font-family-mono. Optional webfont loading via symfinity/font-manager: font-manager-pairing.md.
| Term | Namespace | Example |
|---|---|---|
Semantic neutral |
data-ui-variant="neutral" on colour roles |
Cancel / chrome button |
Mono tone neutral |
mono.neutral.500 palette ramp |
Surface tint — not a button variant |
Typography muted |
typography role variant | Helper text |
Structural text-muted |
--ui-color-text-muted |
Foreground token — not data-ui-variant |
ghost |
data-ui-appearance="ghost" on Button/Link only |
Transparent hover wash — not a semantic colour |