-
Notifications
You must be signed in to change notification settings - Fork 1
EmailRealizer
🌐 This page in: English · Português
Status: the core is BUILT (merged 2026-08-25; ships with the next release).
EmailRealizerandEmailRendererexist ineQuantic.UI.Emailwith the vocabulary below. Still ahead: the real-client matrix (a generated sample against Gmail/Outlook/Apple Mail) and HTML pins once the output settles.
Write the email in C# with the same components a page is written with, and get back HTML an email client will actually render.
A component here is a VisualNode tree that knows nothing about its target. The web realizer turns
it into DOM and CSS; Photon turns it into GPU paint. Email is a third realizer — a target whose
rendering engine happens to be very restrictive, which is architecturally the same shape as a target
with no DOM at all.
Nothing about the authoring layer changes. The same Column, Text, Image and theme tokens, the
same Build(context).
Two pieces are in the tree today, verified by reading them:
-
WebRealizer.Lower(node, theme, typeScale, styles: null)— without a style sink, styles stay inline on the element instead of becoming deduplicated atomic classes. Inline style is email's hardest constraint, and this path already produces it. -
HtmlRenderer.RenderNode(...)turns the lowered tree into an HTML string.
So "C# component → HTML with inline styles" is two existing calls. What is missing is everything about the medium.
| Demand | Why | Where the web realizer differs |
|---|---|---|
Nested <table> layout |
Outlook on Windows renders with Word's engine: no flexbox, no grid |
Row/Column lower to FlexDirection
|
Inline style="" only |
<style> blocks and external CSS are stripped or ignored by several clients |
Atomic classes + a stylesheet (the sink path) |
| Literal colors | CSS custom properties are not resolved | Tokens emit var(--eq-color-…, fallback)
|
| Absolute image URLs or CID |
data: URIs are dropped by Gmail |
Assets assume a served origin |
| A constrained width | ~600px is the convention every client tolerates | Pages are fluid |
| No interaction | No JS, no hover in most clients, no scrolling containers |
Pressable, ScrollView, hover states exist |
A new package, eQuantic.UI.Email, depending only on Primitives and Core — never on
eQuantic.UI.Web, so the two realizers cannot drift into each other.
// Authored exactly like a page.
public sealed class WelcomeEmail(string name) : StatelessComponent
{
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Image(Assets.Logo, width: 132),
Text($"Welcome, {name}", TypeRole.Heading),
Button("Confirm your address", href: "https://…"),
]);
}
// Rendered where it is sent.
EmailMessage message = EmailRenderer.Render(new WelcomeEmail("Edgar"), theme);
// message.Html — table-lowered, fully inline
// message.PlainText — generated from the SAME tree, not written twiceEmailMessage carries both parts because a multipart message needs both, and generating the text
alternative from the tree is the only way it stays in step with the HTML.
M0 — Measure before building. Done — and it changed the plan. Besides the two expected
findings (flex layout, light-dark() colors), a third the plan did not have: typography lives in
eq-type-* classes, and an email has no stylesheet, so every text rendered at one size. M2 grew
to inline the theme's ramp onto every text. The remaining half of M0 is human: the generated sample
against real mailboxes.
M1 — Table lowering. Done. Box, Row, Column and Gap become nested <table>/<tr>/<td> with
cell padding and spacer rows. Alignment maps to align/valign. This is the bulk of the track.
M2 — Inline styles and literal theming. Done (plus the inlined type ramp). Reuse the no-sink path; resolve every token to a literal color and length so nothing depends on custom properties. Density and type scale resolve once, at render.
M3 — Document shell. Done. EmailDocument: doctype, <meta> set, the 600px container, the invisible
preheader line, and the dark-mode meta clients honour.
M4 — Vocabulary fence. Done. Diagnostics naming what an email cannot do — ScrollView, Pressable,
Draggable, hover and focus styling, position: absolute. A Link becomes <a>; a Button
becomes a bulletproof <a> styled as a button. An Image without an absolute URL is an error, not a
broken picture in someone's inbox.
M5 — Plain-text alternative. Done. Walk the same tree for the text/plain part.
The slice went through five review rounds (32 findings) and the survivors are rules worth stating:
-
A property the medium cannot express is fenced, not approximated: gradient ink and
MaxLinesthrow naming the way out — a solid stand-in is a different ink nobody chose, and showing more text than the author bounded is a content divergence. -
A property the medium expresses differently is honoured: container padding (a one-cell
wrapper table),
Crossand per-childAlignSelf(cell alignment), per-corner radii, borders, heading elements,aria-labelfromLink.Label. -
MainAlignis vacuous here, documented rather than fenced: an email table sizes to its content, so there is no free space to distribute — every value renders identically, which is faithful. -
A broken component fails the SEND. Components expand via
Build, not the web'sBuildContained: a describe-box is right on a live page a developer is watching and wrong in a message about to reach a reader's inbox dressed as content. -
Addresses are parsed, not prefix-checked; attribute values are attribute-encoded; a
Linkaround linked runs refuses to nest anchors, naming the choice.
-
No
<style>block, ever, even where a client supports it. One rule beats a matrix of exceptions. - No interactivity. An email is a printed page that happens to have links.
- No client-specific hacks in components. Anything Outlook needs lives in the realizer, never in something an author writes.
- Attachments and delivery are not ours. The output is HTML plus text; sending is the app's job (MailKit, SES, whatever it already uses).
The same two nets the rest of the framework uses:
- HTML pins, regenerated on purpose like the transpiled fixtures, so a layout change is visible as a diff rather than a surprise.
- A client matrix as conformance, run against real clients and recorded — the browser-is-the- arbiter rule applies here even harder, because email clients are further from a spec than browsers are.
🌐 English · Português
🏁 Start here
📱 Write-once
- Write-Once Components
- Declarative Surface
- Photon Engine
- Design System
- Capabilities
- Storage
- Forms
- Code Editor
- Markdown
- Mermaid
- Email Rendering
🏗️ Architecture
⚙️ Compilation
- Compiler
- Compile-Time Evaluation
- Supported C# Features
- External Type Resolution
- Build Flow
- Diagnostics
⚡ Runtime
🔌 Server
🎨 Ecosystem
🚀 Development