eQuantic.UI Styling ArchitectureeQuantic.UI provides a Flutter-inspired developer experience (DX) while leveraging the full power of each rendering target. The styling architecture is built on three pillars:
1.
Abstraction: Components author _what_ to style, as typed C# values, never CSS strings.
2.
One semantics, two targets: the same style vocabulary drives dp values on native (Photon) and CSS on the web.
3.
Performance: build-time class generation and deduplication, with no CSS-in-JS runtime overhead.
1. Styling Components: Typed C#, No CSS Plane
Component styling is authored entirely in typed C# (BoxStyle, StyleDiff, design tokens) and lowered by the atomic style engine: every regular declaration becomes ONE deduplicated atomic class, byte-identical between SSR (C#) and hydration (TS); theme colors reference var(--eq-color-*) custom properties. Hover/focus states, window-size-class adaptivity, sticky positioning and transforms are all part of the vocabulary, with zero JavaScript involved.
Hover is derived, and it is pointer-only
Since 0.2.0-preview.24
The variant palette stays five sub-tokens; hover resolves by DERIVATION: VariantColors.Hover is the per-channel midpoint of Base → Pressed (ColorToken.MidpointWith, the same color color-mix(in srgb, Base 50%, Pressed) lands on), and quiet variants hover on SurfaceSubtle. Components author it as an ordinary BoxStyle.Hover diff, so both realizers already knew how to paint it: the web as an atomic :hover rule, Photon through the pointer pipeline.
Two rules ride the emission itself. Every atomic :hover rule is wrapped in @media (hover: hover), so a touch browser's sticky emulated hover finds no rule, and the gate wraps the rule, never the hash, so SSR and hydration still compare identical class strings. And on Photon, hover is tracked by layout PATH as a CHAIN of everything under the pointer, because CSS :hover matches every ancestor: hovering a tooltip's trigger hovers the tooltip too, and a component rebuild, which replaces every node instance, no longer drops the hover it repainted.
Since 0.2.0-preview.23
Photon paints in CHILD ORDER: a later sibling is on top, and nothing has to be said. CSS stops working that way the moment boxes acquire stacking contexts, so the web realizer spells out what native gives for free, in the design system's own words, never in CSS's.
Elevation decides what is above, not only how deep the shadow is. It drew a shadow and nothing else, which is half a sentence: a raised surface that anything painted after it covers is not raised. Levels 1–5 are the CONTENT plane, and they are the only numbers an author writes.
Chrome is a plane, not a raised card. Anything pinned (a Sticky header, floating or in flow) sits above every content elevation. It used to sit one step above nothing, which is a number competing with other numbers: give elevation the 1–5 range and a merely raised card would out-stack the header and scroll straight over it.
That is why Sticky has no ZIndex and never will. A z-index is CSS vocabulary, and an author who has to pick one is being asked to know which numbers everything else chose. Elevation says how high, the realizer decides how that is spelled, and chrome is not on the same scale as content.
One exception to one declaration, one class, and it is not an exception at all once named: a vendor prefix is not a declaration of its own, it is the same one written for another engine. The pair shares a class whose rule names both, standard property last. Alone in a rule, -webkit-backdrop-filter is dropped whole by an engine that only takes the standard name (insertRule leaves an EMPTY rule in Chromium, measured), so the class was computed, hashed, emitted, put on the element, and did nothing. Since 0.2.0-preview.21.
Since 0.2.0-preview.7
A monospaced run (Text with Mono, a rich run, a CodeBlock) draws in a stack the framework owns, and it lands on an atomic class, which beats a body { font-family } rule, and whose name is a content hash that moves between builds. So there was nowhere to hang your own code face. The hook is a variable:
:root { --eq-font-mono: 'JetBrains Mono', ui-monospace, monospace; }
Every mono run follows it, with the platform stack as the fallback when nothing sets it.
The measurer reads the same variable, and that is not an implementation detail you can ignore if you ship a code editor: CodeEditor places its caret from a measured column advance, so measuring in one face while drawing in another puts the caret beside the character it is on rather than under it.
The full engine (laws, pseudo-states, size classes, the generated stylesheet) is documented in DesignSystem. The abstract vocabulary itself (Box, Row/Column, Grid, Stack, …) is documented in Write-Once Components. Theming = providing an IAppTheme
Themes are typed C# too (colors as light/dark ColorToken pairs, type roles, shape scale, elevation) selected in one line and bridged SSR→client:
builder.Services.AddUI(options =>
options.ScanAssembly(typeof(Program).Assembly)
.UseTheme(PhotonTheme.Instance); // or MaterialTheme.FromSeed(color)
Two implementations ship: PhotonTheme (the default) and MaterialTheme (real Material 3, including dynamic color from a seed). Swap the theme and every component rebrands, server and client, web and native. Runtime light/dark switching is a service (IThemeController); see DesignSystem. Since 0.2.0-preview.13
BorderColor = theme.Border,
BorderSides = BorderSides.Top, // a rule above a section
BorderColor = theme.Colors(Variant.Primary).Base,
BorderSides = BorderSides.Start, // an accent bar down a callout
BorderSides.All by default, so every box written before this renders byte for byte the same: a full border still emits the one-declaration shorthand and its single atomic class.
None of these is a Divider. A Divider is a sibling between two things; these belong to the box itself, and the only way to draw one was a Row wrapping a one-dp Box, which is a layout lie about what the design meant.
Start and End rather than Left and Right, matching EdgeInsets: they mirror in a right-to-left reading, and an accent bar that stays on the left when the text flows the other way is on the wrong side of it.
FENCE With a corner radius, a partial border differs slightly between targets at the corner where a present edge meets an absent one: the web mitres it, Photon squares it. At radius 0 (a rule, an accent bar, a table cell, which is what partial borders are for) the two are identical. Use BorderSides.All for a rounded outline.
The low-level web layer (eQuantic.UI.Core) mirrors the DOM 1:1. Every HtmlElement exposes the attributes the browser has:
public abstract class HtmlElement : IComponent {
/// <summary>Raw CSS classes (space-separated).</summary>
public string? ClassName { get; set; }
/// <summary>Inline styles for dynamic values (e.g., coordinates, colors from DB).</summary>
public HtmlStyle? Style { get; set; }
This contract means the web layer does not enforce any CSS framework, it renders HTML attributes. DynamicElement goes further and renders any tag with any attributes. Standard CSS classes from any stylesheet work natively:
new DynamicElement("aside") { ClassName = "my-sidebar" }
The framework ships exactly one styling engine, the typed C# pipeline above. There is no bundled CSS framework and no adapter layer: a page that needs hand-written markup uses the escape hatch (HtmlElement/DynamicElement + ClassName), and any stylesheet the app brings (its own CSS file, a utility framework it builds itself) is referenced like on any web project:
builder.Services.AddUI(options =>
options.ConfigureHtmlShell(shell => shell.AddStylesheet("/css/app.css"));
The framework neither generates nor processes that CSS; it is the app's own build concern.
eQuantic.UI.Primitives
Typed styles, design tokens, the abstract vocabulary: the styling source of truth.
eQuantic.UI.Core
The DOM-mirror layer: HtmlElement, ClassName, HtmlStyle, DynamicElement.
eQuantic.UI.Web
The web realizer + atomic style engine (StyleAtomizer) + generated stylesheet.