For years, Symfony teams solved UI scaling one of two ways.
Either you adopted a JavaScript build pipeline — Webpack Encore, Vite, npm scripts — and a utility CSS framework like Tailwind.
Or you stayed "PHP-first" and watched every project grow its own app.css, its own spacing scale, its own dark-mode hacks, and its own Bootstrap overrides.
Both paths work. Neither scales cleanly across agencies, products, and long-lived admin UIs.
UI Kernel (symfinity/ui-kernel) is Symfinity's answer on the Symfony side: treat design tokens as infrastructure — resolved in PHP, emitted as CSS custom properties, consumed by Twig and component CSS — without making Node the gatekeeper of your theme.
This article explains the CSS story first, then where Symfony fits in.
The problem tokens solve
Hard-coded hex values in templates do not scale.
When primary blue lives in forty places, rebranding is a grep exercise. When dark mode is bolted on with @media (prefers-color-scheme: dark) and ad hoc overrides, contrast breaks in corners nobody tested. When spacing is "whatever looked fine in Figma," components drift apart.
Design tokens are the fix: named, semantic variables (--color-text, --space-md, --radius-lg) that components reference instead of raw values.
That idea is not new. What changed recently is how we author and validate those tokens in CSS — especially for colour.
Why OKLCH matters for palettes
Classic spaces — RGB, HSL — are easy to compute but bad at matching human perception.
A "50% lightness" yellow and a "50% lightness" blue do not look equally bright. Darkening with hsl() or Sass darken() shifts hue and brightness in unpredictable ways. Building a ten-step brand ramp that stays accessible was always guesswork.
OKLCH (lightness, chroma, hue in the Oklab model) is perceptually uniform: equal steps on the L axis feel like equal brightness changes across hues. That makes ramps, states (hover, active), and dark-mode pairs far more predictable.
In modern CSS you can write tokens directly:
:root {
--text: oklch(0.30 0.03 260);
--surface: oklch(0.97 0.01 260);
--accent: oklch(0.60 0.14 255);
}
Progressive enhancement still matters — ship sRGB fallbacks, layer OKLCH behind @supports:
:root {
--text: #222326;
--accent: #005fcc;
}
@supports (color: oklch(0.5 0.1 0)) {
:root {
--text: oklch(0.30 0.03 260);
--accent: oklch(0.60 0.14 255);
}
}
For state colours, color-mix(in oklch, …) keeps blends on the perceptual axis instead of muddy RGB averages.
Contrast: WCAG today, APCA tomorrow
Accessibility checks still anchor on WCAG 2.x contrast ratios (4.5:1 body text, 3:1 large text/UI). OKLCH helps because L tracks perceived lightness — pairs with sufficient L separation tend to pass, which is why token-driven systems can enforce contrast at generation time instead of in every component file.
APCA (Advanced Perceptual Contrast Algorithm) is the direction of travel for WCAG 3. Good practice in 2026: design with APCA awareness, audit with WCAG for compliance paperwork. They usually agree; where they diverge, APCA often reflects real-world legibility better (thin type, dark UI, wide gamut).
Tools worth bookmarking
These are the references I reach for when building or reviewing token palettes — independent of Symfony:
| Tool | URL | Why |
|---|---|---|
| OKLCH Color Picker & Converter | oklch.com | Pick, convert, P3/Rec2020 preview, palette building with APCA — by Evil Martians |
| Evil Martians OKLCH ecosystem guide | Exploring the OKLCH ecosystem | Harmonizer, apcach, and related tooling |
| Atmos OKLCH picker | atmos.style/color-picker/oklch | Fast format conversion + WCAG/APCA hints |
| HubTools color picker | hub-tools.com/tools/color-picker | OKLCH/LAB/P3, harmonies, CVD simulation, W3C token export |
| APCA introduction | APCA easy intro | Readable explainer for the contrast model |
| W3C Design Tokens Community Group | designtokens.org | Format spec for portable token files |
Further reading (CSS and design systems)
These DEV Community articles cover the CSS side better than any Symfony doc should duplicate:
-
CSS Color Picker: HEX, RGB & HSL Converter Guide for Developers — DEV Community — OKLCH ramps,
color-mix(in oklch, …),@supportsfallbacks, dark mode viaprefers-color-scheme(pair with Making Animations and Transitions Accessible forprefers-reduced-motion) -
Color Spaces for Developers: Why Your Eyes Disagree With The Math — Michael Lip — perceptual uniformity, relative colour syntax (
oklch(from var(…) …)),color-mix -
Designing accessible color systems and ensuring contrast across themes — Beefed AI — semantic tokens, OKLCH,
prefers-color-scheme,prefers-contrast/ forced-colors in the test matrix, Style Dictionary handoff - The Mystery of Tailwind Colors (v4) — Mat Frana — practical OKLCH palette rebuild (lightness/chroma curves, P3 gamut); migration narrative rather than Token Studio/APCA
-
Building a Figma to GitHub token pipeline that actually works — Alexander's Studio — W3C
$value/$typeJSON → Style Dictionary → CSS variables (useful context even when Symfony owns emission)
Symfony-adjacent (same author, typography chain):
From CSS files to Symfony infrastructure
Node-centric pipelines (Style Dictionary, Tailwind config, theme JSON → build step) fit design agencies. They hurt Symfony teams who want Flex install, YAML config, and AssetMapper without a second repository for tokens.
UI Kernel inverts the default:
- Tokens live as data — W3C DTCG-shaped theme layers (colour, spacing, radius, motion, typography references).
- PHP resolves and validates — lineage (Balanced, Semantic, Utility), light/dark variants, OKLCH ramp math, contrast-aware semantic colours.
-
CSS is emitted server-side — custom properties on
[data-theme="…"], ready for cache warmup. - Twig wires the page — boot script + theme CSS in the layout; no Encore entry for "theme variables."
Minimal layout:
{# templates/base.html.twig #}
<head>
{{ ui_kernel_theme_boot_script() }}
{{ ui_kernel_css()|raw }}
</head>
Configuration stays in familiar Symfony paths:
# config/packages/symfinity_ui_kernel.yaml
symfinity_ui_kernel:
default_theme: semantic
default_variant: semantic
Install is Composer + Flex — same story as any other bundle:
composer require symfinity/ui-kernel
What you do not need for theming: Tailwind as the token source of truth, a Sass pipeline for variables, or Encore/Vite solely to compile :root { --… }.
What you still use Symfony for: AssetMapper for component JS/CSS paths, Stimulus for micro-interactions, UX Twig Components for leaves — AssetMapper is plumbing, not your design system.
How this stays scalable
Semantic tokens, not utility soup
Components and UX Blocks reference roles and semantic variables (data-ui-role, --ui-color-accent, spacing rhythm) — not p-4 text-gray-600 copied across Twig files.
The kernel owns the look spine; symfinity/ux-blocks-* packages own component CSS that consumes those variables. Boundary stays explicit: kernel emits tokens and global profile rules; blocks emit role selectors.
One theme graph, many surfaces (direction)
The same resolved token graph can feed:
- Web layouts (today)
- Email HTML with inline-safe subsets (horizon)
- PDF/print (horizon)
- CLI/TUI structured output (horizon)
That is the payoff of server-resolved tokens versus "whatever landed in public/build/app.css last Tuesday."
OKLCH inside, CSS outside
Internally, Symfinity generates palettes in OKLCH for perceptual ramps and ref resolution. Public CSS still ships browser-ready strings (sRGB hex/rgb(), with P3 where supported). You get palette math without asking every integrator to hand-author oklch() literals in YAML.
Works with what you already run
UI Kernel complements Symfony UX — it does not replace Live Components, Turbo, or Stimulus.
Typical Stage A path:
- Install kernel for tokens on the layout shell.
- Drop UX Blocks into existing Twig.
- Add font-manager when typography tokens need real webfonts.
Plain Twig + your own CSS remains valid for marketing pages. The stack targets long-lived product and admin UI where theme drift hurts.
When Tailwind or Encore still make sense
Honest scope:
- Tailwind — excellent for marketing microsites, rapid prototypes, or teams already standardized on utility CSS. Symfinity does not ask you to rip it out on day one.
- Encore/Vite — still right when you need a heavy JS application bundle, legacy React/Vue islands, or org-wide frontend tooling unrelated to tokens.
UI Kernel draws a line: theming and design-system variables are Symfony infrastructure, not npm devDependencies.
If your pain is "every client has a different primary colour and our admin UI looks nothing like last year's project," tokens in PHP beat another tailwind.config.js fork.
Try it locally
After adding the [symfinity/recipes]:(https://github.com/symfinity/recipes) Flex endpoint:
composer require symfinity/ui-kernel
If you find it useful, star the repository and share it with your Symfony community.
What's next
Don't miss the introduction series to Symfinity:
- Part 1: Why building Symfony-native packages instead of doing infrastructure again and again
- Part 2: Why Symfony projects feel more fragmented than ever
- Part 3: The rule behind every Symfinity package
More package-level deep dives:
- Font Manager: Multi-Format Font Export for Symfony
- Omnia Ipsum: Unified Placeholder Content for Symfony
Articles on further Symfinity package tiers are planned, this is just the beginning.
Explore packages and source at github.com/symfinity.
Top comments (0)