Skip to content

HTML Email Rendering

The Pragmatic.Documents.Email package renders an EmailModel to email-client-safe HTML. The model is defined in Pragmatic.Email.Model and uses an idiomatic section → column → node builder API.


HTML email is not HTML. The big differences:

HTMLHTML email
CSS classes, stylesheetsInline styles (Outlook ignores <style> half the time)
Modern layout (flex, grid)Nested <table>s — tables are the safe layout primitive
No fixed widthWrapper width (typically 600px) for predictable rendering
Preheader in <head>Hidden preheader as the first text element
Images rendered freelyImages often blocked — need alt text and fallback

Pragmatic.Documents.Email abstracts all of this. You describe sections and columns; the renderer produces HTML that works in Gmail, Outlook (including MSO-conditional tables for 2007+), Apple Mail, iOS/Android clients, and webmail.


public sealed class EmailHtmlRenderer
{
public string Render(EmailModel model);
public void RenderTo(TextWriter writer, EmailModel model);
}
public sealed class EmailBuilder
{
public EmailBuilder Subject(string subject);
public EmailBuilder Preheader(string preheader);
public EmailBuilder Language(string language);
public EmailBuilder Width(int width);
public EmailBuilder BackgroundColor(string color);
public EmailBuilder WrapperBackgroundColor(string color);
public EmailBuilder FontFamily(string fontFamily);
public EmailBuilder FontSize(int fontSize);
public EmailBuilder TextColor(string color);
// Section types
public EmailBuilder Section(Action<SectionBuilder> configure);
public EmailBuilder FullWidthSection(Action<ColumnBuilder> configure, string? backgroundColor = null);
public EmailBuilder Hero(Action<HeroBuilder> configure, string? backgroundColor = null);
public EmailBuilder TwoColumns(Action<ColumnBuilder> left, Action<ColumnBuilder> right, ...);
public EmailBuilder ThreeColumns(...);
public EmailBuilder Article(Action<ArticleBuilder> configure, string? backgroundColor = null);
public EmailBuilder Footer(Action<ColumnBuilder> configure, string? backgroundColor = null);
public EmailBuilder SocialBar(IEnumerable<SocialLink> links, string? backgroundColor = null);
public EmailModel Build();
}

using Pragmatic.Email.Model;
using Pragmatic.Documents.Email;
var email = new EmailBuilder()
.Subject("Welcome to Pragmatic")
.Preheader("Your account is ready.")
.Width(600)
.Section(s => s
.Column(c => c
.Heading("Welcome, Alice!")
.Text("Thanks for signing up.")
.Button("Get started", "https://app.acme.com/start")))
.Build();
string html = new EmailHtmlRenderer().Render(email);

Sections are the default container — they accept one or more columns.

.Section(s => s
.BackgroundColor("#ffffff")
.Padding(24)
.Column(c => c
.Heading("Section heading")
.Text("Section body.")))

A hero has background colour / image and a prominent heading.

.Hero(h => h
.BackgroundImage("https://cdn.acme.com/banner.jpg")
.Heading("Spring sale")
.Text("Save up to 30%")
.Button("Shop now", "https://shop.acme.com"))

Side-by-side columns that collapse to a vertical stack on narrow screens (responsive CSS, no media-query gymnastics in your code).

.TwoColumns(
left: l => l.Heading("Feature A").Text("Description of feature A."),
right: r => r.Heading("Feature B").Text("Description of feature B."))

A classic article block with a heading, body text, and optional CTA.

.Article(a => a
.Heading("What's new in 0.8")
.Paragraph("40 modules, 42 samples, end-to-end Showcase.")
.Button("Read the release notes", "https://pragmaticdesign.net/0.8"))
.Footer(f => f
.Text("You're receiving this email because you signed up at acme.com.",
color: "#666", align: EmailTextAlign.Center)
.Text("123 Main Street, Lisbon, Portugal", color: "#666", align: EmailTextAlign.Center))
.SocialBar(new[]
{
new SocialLink { Platform = "twitter", Url = "https://twitter.com/acme" },
new SocialLink { Platform = "github", Url = "https://github.com/acme" },
})

Inside a column you assemble EmailNodes via the ColumnBuilder:

MethodProduces
.Heading(content, level, align)<h1..h6> with inline style
.Text(content, align, color)Paragraph with inline style
.Image(src, alt, width, link)<img> with optional wrapping <a>
.Button(text, href, bg)Bulletproof button (MSO-conditional)
.Spacer(height)Vertical spacing row
.Divider(color)Horizontal rule

Each helper returns the builder, so you can chain them.


The renderer emits:

  • DOCTYPE — HTML 4.01 Transitional (most compatible)
  • Meta tags — charset UTF-8, viewport, colour-scheme
  • MSO conditionals<!--[if mso]> blocks for Outlook-specific layout
  • Inline CSS on every element — no reliance on <style>
  • Preheader — hidden text as the first body element
  • Fallbacks — buttons are bulletproof (anchor + table + MSO VML), images have alt text, font stacks include email-safe fallbacks

Tested against Gmail (web + iOS + Android), Outlook 2016/2019/365, Apple Mail, iOS Mail, Yahoo Mail, Outlook.com. For older Outlook (2007-2010) MSO-conditional blocks keep the layout intact.


Pragmatic.Documents.Email produces HTML — it doesn’t send email. Hand the result to:

  • Pragmatic.Email — transactional email with transport abstraction (SMTP, SendGrid, SES)
  • Your existing SMTP library
  • A provider SDK (Postmark, SendGrid, Mailgun)
var email = BuildVerificationEmail(userEmail, verifyLink);
string html = new EmailHtmlRenderer().Render(email);
await smtpClient.SendAsync(new Message
{
Subject = email.Subject,
To = userEmail,
HtmlBody = html,
TextBody = StripToPlainText(html), // always include a text alternative
});

For multipart alternatives (text/plain + text/html), render both: the model carries the subject and preheader; convert the column content to plain text yourself.


  • No support for AMP email (<amp-carousel>, forms) — plain HTML email only.
  • No built-in i18n — use Pragmatic.Documents.Templating.I18N + a template.
  • No A/B test variants or preview images — those belong to your ESP.
  • Dark-mode tuning is opt-in per design — the renderer uses color-scheme: light dark but doesn’t auto-adjust colour choices.