Email Channel Conformance Spec
Conformance tests for the Norq email compiler. The email compiler takes Markdown input (with YAML frontmatter) and produces MJML output, which is then rendered to HTML by the mrml library. Conformance tests validate the intermediate MJML output, not the final HTML.
Each example block specifies input markdown and one or more expected output
fragments (substrings that must appear in the compiled MJML).
Format:
- Opening fence: 32 backticks followed by
example channel=email - Input markdown (first section, before the first
.separator) .separator line- One or more expected fragments, each separated by
.lines - Closing fence: 32 backticks
The test runner compiles each input using the default EmailTheme with
serde_json::Value::Null as data and checks that every expected fragment
appears as a substring of the EmailOutput.mjml field.
Document Structure
Frontmatter and MJML boilerplate
Every email template requires a subject field in the YAML frontmatter. The
compiler wraps the entire output in an <mjml> document containing
<mj-head> (with <mj-attributes> defining defaults for fonts, colors, and
button styling) and <mj-body>. The body background-color and width come from
the theme.
---
subject: Welcome
---
Hello
.
<mjml>
.
<mj-head>
.
<mj-attributes>
.
<mj-text font-size="16px" line-height="1.6" color="#3f3f46" />
.
<mj-body background-color="#fafafa" css-class="email-bg" width="600px">Preheader
The optional preheader frontmatter field emits an <mj-preview> tag inside
<mj-head>, which email clients show as preview text in the inbox list.
---
subject: Order shipped
preheader: Your package is on the way
---
Your order has shipped.
.
<mj-preview>Your package is on the way</mj-preview>Default heading styles
The <mj-head> includes a <mj-style> block defining default heading sizes
and weights. These CSS rules apply to all headings in the email body unless
overridden by block attributes.
---
subject: Test
---
# Title
.
h1 { font-size: 32px; font-weight: 700; color: #09090b; margin: 0 0 12px 0; }
.
h2 { font-size: 24px; font-weight: 700; color: #09090b; margin: 0 0 10px 0; }
.
h3 { font-size: 20px; font-weight: 600; color: #09090b; margin: 0 0 8px 0; }Button defaults in mj-attributes
The <mj-attributes> block sets default button styling: background-color from
the theme's buttonColor, text color from buttonTextColor, border-radius of
3px, font-weight 600, font-size 13px, and inner-padding of 10px 25px.
---
subject: Test
---
Hello
.
<mj-button background-color="#18181b" color="#fafafa" border-radius="3px" font-weight="600" font-size="13px" inner-padding="10px 25px" align="center" />Paragraphs
Basic paragraph
A paragraph compiles to a <p> tag inside <mj-text>, wrapped in an
<mj-section> with css-class email-content and padding 20px 0.
Consecutive text-like nodes (paragraphs, headings, lists, blockquotes, code
blocks) are batched into a single TextRun, sharing one <mj-section>.
---
subject: Test
---
Hello world
.
<mj-section css-class="email-content" padding="20px 0"><mj-column><mj-text><p>Hello world</p></mj-text></mj-column></mj-section>Consecutive paragraphs batch into a single section
Multiple consecutive paragraphs are grouped into one TextRun. They share a
single <mj-section> and <mj-text>, with each paragraph as a separate
<p> tag.
---
subject: Test
---
First paragraph
Second paragraph
.
<mj-text><p>First paragraph</p><p>Second paragraph</p></mj-text>Inline formatting
Paragraphs support standard Markdown inline formatting: bold (**text**),
italic (*text*), inline code (`code`), and links.
---
subject: Test
---
This is **bold** and *italic* and `code` text.
.
<p>This is <strong>bold</strong> and <em>italic</em> and <code>code</code> text.</p>Links in paragraphs
Links render as <a> tags with the theme's brand color applied as an inline
style="color:..." attribute within text runs.
---
subject: Test
---
Visit [our site](https://example.com) today.
.
<a href="https://example.com" style="color:#18181b">our site</a>Headings
Heading levels
Headings h1 through h4 compile to their corresponding HTML heading tags inside
<mj-text>. They are batched with adjacent paragraphs into the same TextRun
section.
---
subject: Test
---
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
.
<h1>Heading 1</h1>
.
<h2>Heading 2</h2>
.
<h3>Heading 3</h3>
.
<h4>Heading 4</h4>Images
An image is written using standard Markdown syntax: . When an
image is the sole meaningful content of a paragraph (ignoring surrounding
whitespace), the compiler emits an <mj-image> element. If the image appears
inline alongside other text or formatting, it is emitted as an HTML <img> tag
inside an <mj-text> block instead.
This section covers only the <mj-image> path — the common case for email
templates.
Basic images
A paragraph containing only an image compiles to an <mj-image> wrapped in an
<mj-section> and <mj-column>. The default padding is 10px 25px, matching
MJML's built-in <mj-image> default.
---
subject: Test
---

.
<mj-image src="https://example.com/product.jpg" alt="Product photo" padding="10px 25px" border="none" />The border="none" attribute is always emitted to prevent default borders in
some email clients.
An empty alt text is allowed but the linter will warn about it
(a11y/missing-alt):
---
subject: Test
---

.
<mj-image src="https://example.com/hero.jpg" alt="" padding="10px 25px" border="none" />Image width
The {width="N"} attribute sets the image display width. Values without a unit
are treated as pixels. The compiler appends px if not already present.
---
subject: Test
---
{width="600"}
.
<mj-image src="https://example.com/banner.jpg" alt="Banner" padding="10px 25px" border="none" width="600px" />A value with an explicit unit is passed through unchanged:
---
subject: Test
---
{width="50px"}
.
width="50px"Image alignment
The {align="left|center|right"} attribute controls horizontal alignment
within the column. Without this attribute, the image inherits the column's
default alignment (typically center in MJML).
---
subject: Test
---
{align="left"}
.
<mj-image src="https://example.com/logo.png" alt="Logo" padding="10px 25px" border="none" align="left" />Image padding
The {padding="..."} attribute overrides the default 10px 25px padding.
It accepts named tokens (none, compact, normal, spacious) or raw CSS
shorthand values ("0", "16 32", "8 16 8 16").
A zero-padding image is flush with the edges of its column:
---
subject: Test
---
{padding="0"}
.
<mj-image src="https://example.com/hero.jpg" alt="Hero" padding="0px" border="none" />Named tokens resolve to pixel values:
---
subject: Test
---
{padding="spacious"}
.
padding="40px 32px"Image border radius
The {border-radius="N"} attribute rounds the image corners.
---
subject: Test
---
{border-radius="50%" width="80"}
.
<mj-image src="https://example.com/avatar.jpg" alt="Avatar" padding="10px 25px" border="none" width="80px" border-radius="50%" />Clickable images
A link wrapping an image — [](link-url) — compiles to
a single <mj-image> with an href attribute. The link's URL becomes the
click target; the image's URL is the src.
Clickable images have a default padding of 0 (not 10px 25px), because
they typically serve as hero banners or logos where edge-to-edge display is
desired.
---
subject: Test
---
[](https://example.com/shop)
.
<mj-image src="https://example.com/banner.jpg" alt="Shop now" padding="0" border="none" href="https://example.com/shop" />Attributes can be placed on the link. They apply to the generated
<mj-image>. Link attributes take priority over image attributes when
both are present:
---
subject: Test
---
[](https://example.com){width="200" padding="10"}
.
<mj-image src="https://example.com/logo.png" alt="Logo" padding="10px" border="none" href="https://example.com" width="200px" />Section wrapping
At the top level of a template (outside any directive), an image paragraph is
wrapped in its own <mj-section> with CSS class email-content and padding
10px 25px:
---
subject: Test
---

.
<mj-section css-class="email-content" padding="10px 25px"><mj-column><mj-image src="https://example.com/hero.jpg" alt="Hero" padding="10px 25px" border="none" /></mj-column></mj-section>Images inside text
When an image appears alongside text in the same paragraph (not as the sole
content), it does NOT compile to <mj-image>. Instead, it renders as an HTML
<img> tag inside an <mj-text> block. This is because <mj-image> is a
block-level MJML element that cannot be mixed with text.
---
subject: Test
---
Check out this  inline icon.
.
<img
.
iconButtons
Basic button
A link with the {button} attribute compiles to an <mj-button> element
wrapped in its own <mj-section>. The default variant is primary, using the
theme's buttonColor as background and buttonTextColor as text color.
---
subject: Test
---
[Click here](https://example.com/cta){button}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/cta" background-color="#18181b" color="#fafafa">Click here</mj-button>Secondary variant
{button.secondary} produces a transparent-background button with a 2px solid
border in the theme's secondary color. The text uses secondaryTextColor.
---
subject: Test
---
[Cancel](https://example.com/cancel){button.secondary}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/cancel" background-color="transparent" color="#71717a" border="2px solid #e4e4e7">Cancel</mj-button>Danger variant
{button.danger} uses the theme's danger color (#ef4444) as background.
---
subject: Test
---
[Delete](https://example.com/delete){button.danger}
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/delete" background-color="#ef4444" color="#ffffff">Delete</mj-button>Success variant
{button.success} uses the theme's success color (#22c55e) as background.
---
subject: Test
---
[Confirm](https://example.com/confirm){button.success}
.
background-color="#22c55e" color="#ffffff"Warning variant
{button.warning} uses the theme's warning color (#f59e0b) as background.
---
subject: Test
---
[Caution](https://example.com/caution){button.warning}
.
background-color="#f59e0b" color="#ffffff"Custom button colors with bg and color
When both bg and color attributes are present on a button, bg sets the
background and color sets the text color. This is the standard path for
custom button colors.
---
subject: Test
---
[Custom](https://example.com){button bg="#111111" color="#ffffff"}
.
background-color="#111111" color="#ffffff"Legacy color-only semantics
When color is specified alone (without bg), it is treated as the
background color for legacy compatibility. The text defaults to white.
---
subject: Test
---
[Legacy](https://example.com){button color="#ff0000"}
.
background-color="#ff0000" color="#ffffff"Full-width button
The {full} attribute or {width="full"} makes the button stretch to 100%
of its container width.
---
subject: Test
---
[Full Width](https://example.com){button full}
.
width="100%"Custom border radius
The {radius="N"} attribute overrides the default 3px border-radius. The
value is emitted with a px suffix.
---
subject: Test
---
[Rounded](https://example.com){button radius="20"}
.
border-radius="20px"Lists
Unordered list
An unordered list compiles to <ul><li>...</li></ul> inside a text run's
<mj-text> block.
---
subject: Test
---
- Item one
- Item two
- Item three
.
<ul><li>Item one</li><li>Item two</li><li>Item three</li></ul>Ordered list
An ordered list compiles to <ol><li>...</li></ol>.
---
subject: Test
---
1. First
2. Second
3. Third
.
<ol><li>First</li><li>Second</li><li>Third</li></ol>Code Blocks
Code block without language
Fenced code blocks without a language specifier compile to a monospace <pre>
with a dark background, wrapped in <mj-text>. The code block uses hardcoded
styling for email-safe rendering.
---
subject: Test
---
```
const x = 42;
```
.
<pre style="background:#2b303b;color:#c0c5ce;padding:16px;border-radius:8px;overflow-x:auto;font-family:monospace;font-size:14px;line-height:1.5"><code>
.
const x = 42;Dividers
Basic divider
A Markdown thematic break (***) compiles to an <mj-divider /> wrapped in
an <mj-section> with padding 10px 25px.
---
subject: Test
---
***
.
<mj-section css-class="email-content" padding="10px 25px"><mj-column><mj-divider /></mj-column></mj-section>Divider with attributes
The {color="..." thickness="..." width="..."} block attributes customize the
divider. Color tokens are resolved through the theme. Thickness and width
values get a px suffix if no unit is present.
---
subject: Test
---
***
{color="#cccccc" thickness="2" width="50%"}
.
<mj-divider width="50%" border-color="#cccccc" border-width="2px" />Block Attributes
Block attributes use the {key="value"} syntax on the line immediately
following a paragraph or heading. They resolve to inline CSS style attributes
on the generated HTML element.
Size tokens
The size attribute maps named tokens to pixel font-sizes:
| Token | Value |
|---|---|
xs |
12px |
sm |
14px |
md |
16px |
lg |
18px |
xl |
20px |
2xl |
24px |
3xl |
30px |
4xl |
36px |
---
subject: Test
---
Large text
{size="lg"}
.
<p style="font-size:18px">Large text</p>---
subject: Test
---
Extra large heading
{size="3xl"}
.
style="font-size:30px"---
subject: Test
---
Huge heading
{size="4xl"}
.
style="font-size:36px"Color tokens
The color attribute maps named tokens to theme colors:
| Token | Default value |
|---|---|
brand |
#18181b |
muted |
#71717a |
danger |
#ef4444 |
success |
#22c55e |
warning |
#f59e0b |
Raw hex values (starting with #) are passed through.
---
subject: Test
---
Subdued text here.
{color="muted"}
.
<p style="color:#71717a">Subdued text here.</p>---
subject: Test
---
Error message
{color="danger"}
.
color:#ef4444Background tokens
The bg attribute maps named tokens to theme background colors:
| Token | Default value |
|---|---|
brand |
#18181b |
card |
#f4f4f5 |
highlight |
#f59e0b |
Raw hex values are passed through.
---
subject: Test
---
Card text
{bg="card"}
.
background-color:#f4f4f5Weight tokens
The weight attribute maps to CSS font-weight values:
| Token | Value |
|---|---|
light |
300 |
normal |
400 |
medium |
500 |
semibold |
600 |
bold |
700 |
---
subject: Test
---
Bold paragraph text
{weight="bold"}
.
<p style="font-weight:700">Bold paragraph text</p>Spacing tokens
The spacing attribute controls line-height. Named tokens map to fixed
values, and numeric values in the range 0.5-3.0 are passed through directly.
| Token | Value |
|---|---|
tight |
1.5 |
normal |
1.6 |
relaxed |
1.8 |
loose |
2.0 |
---
subject: Test
---
Relaxed line spacing
{spacing="relaxed"}
.
<p style="line-height:1.8">Relaxed line spacing</p>Padding tokens (block-level)
The padding attribute on paragraphs resolves to inline CSS padding. These
are the block-level padding tokens (not to be confused with the MJML-level
padding tokens used for images and directives).
| Token | Value |
|---|---|
none |
0 |
compact |
8px 0 |
normal |
16px 0 |
spacious |
32px 0 |
---
subject: Test
---
Spacious padding paragraph
{padding="spacious"}
.
<p style="padding:32px 0">Spacious padding paragraph</p>Multiple attributes
Attributes can be combined freely. They compile to a semicolon-separated
style attribute.
---
subject: Test
---
Styled paragraph
{size="lg" align="center" weight="bold"}
.
style="font-size:18px;text-align:center;font-weight:700"Directives
:::highlight
The :::highlight directive produces an <mj-section> containing a column
with the theme's brand color (#18181b) as background, white text, and
hardcoded font-weight="600" and border-radius="8px". The column carries
the CSS class email-highlight.
---
subject: Test
---
::: highlight
Special offer inside!
:::
.
<mj-column background-color="#18181b" css-class="email-highlight" border-radius="8px" padding="20px 24px"><mj-text align="left" color="#ffffff" font-weight="600">Custom background via {bg="..."}:
---
subject: Test
---
::: highlight {bg="#0000ff"}
Blue highlight
:::
.
background-color="#0000ff" css-class="email-highlight"
.
color="#ffffff" font-weight="600":::callout
The :::callout directive produces an <mj-section> with a column using the
theme's card color (#f4f4f5) as background. The column carries the CSS class
email-card with border-radius="8px".
---
subject: Test
---
::: callout
This is a callout box.
:::
.
css-class="email-card" border-radius="8px"
.
<p>This is a callout box.</p>Custom background color on callout is passed through:
---
subject: Test
---
::: callout {bg="#eff6ff"}
Blue callout
:::
.
background-color="#eff6ff" css-class="email-card":::action
The :::action directive extracts links from its children and renders each as
an <mj-button> in its own <mj-section>. Links can specify a variant
({primary}, {secondary}, {danger}, {success}, {warning}) via
attributes. The default variant is primary.
---
subject: Test
---
::: action
[Get Started](https://example.com/start)
:::
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/start" background-color="#18181b" color="#fafafa">Get Started</mj-button>A {danger} variant button uses the danger color:
---
subject: Test
---
::: action
[Delete Account](https://example.com/delete){danger}
:::
.
<mj-button css-class="norq-btn" align="center" href="https://example.com/delete" background-color="#ef4444" color="#ffffff">Delete Account</mj-button>Custom colors inside :::action using bg and color attributes:
---
subject: Test
---
::: action
[Custom Button](https://example.com){bg="#111111" color="#ffffff"}
:::
.
background-color="#111111" color="#ffffff"Full-width button inside :::action:
---
subject: Test
---
::: action
[Full Button](https://example.com){full}
:::
.
width="100%":::columns + :::col
The :::columns directive produces an <mj-section> containing multiple
<mj-column> elements — one per :::col child. Each column can specify
width, vertical alignment, and background color.
Two equal columns (no explicit width defaults to MJML auto-sizing):
---
subject: Test
---
::: columns
::: col
Left column content
:::
::: col
Right column content
:::
:::
.
<mj-section css-class="email-content"
.
<mj-column>
.
Left column content
.
Right column contentColumn with explicit fractional width {width="2/3"} converts to 66.67%:
---
subject: Test
---
::: columns
::: col {width="2/3"}
Wide column
:::
::: col {width="1/3"}
Narrow column
:::
:::
.
width="66.67%"
.
width="33.33%"Column with vertical alignment:
---
subject: Test
---
::: columns
::: col {valign="middle"}
Centered vertically
:::
::: col
Other content
:::
:::
.
vertical-align="middle"Column with background color:
---
subject: Test
---
::: columns
::: col {bg="#f0f0f0"}
Tinted column
:::
::: col
Normal column
:::
:::
.
background-color="#f0f0f0"Mobile stacking control
Columns with {mobile="inline"} emit <mj-group> to prevent stacking.
---
subject: Test
---
::: columns {mobile="inline"}
::: col {width="1/3"}
A
:::
::: col {width="1/3"}
B
:::
::: col {width="1/3"}
C
:::
:::
.
<mj-group>:::header
The :::header directive is partitioned out of the content flow and emitted
as a separate <mj-section> with the CSS class email-header. Default
alignment is center and default padding is 32px 32px 24px 32px. The
background defaults to the theme's background color.
---
subject: Test
---
::: header
My Company
:::
Welcome to our service.
.
<mj-section background-color="#fafafa" css-class="email-header" padding="32px 32px 24px 32px"><mj-column><mj-text padding="0" align="center">
.
My Company:::footer
The :::footer directive is partitioned out of the content flow and emitted
as a separate <mj-section> with CSS class email-footer. It uses a smaller
font size (13px), line-height 1.5, and the theme's secondary text color
(#71717a). Default padding is 24px 32px 32px 32px.
---
subject: Test
---
::: footer
Unsubscribe | Privacy Policy
:::
.
<mj-section background-color="#fafafa" css-class="email-footer" padding="24px 32px 32px 32px"><mj-column><mj-text padding="0" font-size="13px" line-height="1.5" color="#71717a" align="center">:::hero
The :::hero directive with a {url="..."} attribute emits an <mj-hero>
element with mode="fluid-height" and the URL as background-url. Default
padding is 100px 0px and default text color is white.
---
subject: Test
---
::: hero {url="https://example.com/banner.jpg"}
Hero headline
:::
.
<mj-hero mode="fluid-height" background-url="https://example.com/banner.jpg" padding="100px 0px"><mj-text padding="0" align="center" color="#ffffff">
.
Hero headline:::fields
The :::fields directive parses Key: Value lines and produces an HTML table
inside <mj-text>. Keys are rendered with font-weight:bold.
---
subject: Test
---
::: fields
Status: Shipped
Carrier: FedEx
:::
.
<td style="font-weight:bold;padding:4px 8px">Status</td><td style="padding:4px 8px">Shipped</td>
.
<td style="font-weight:bold;padding:4px 8px">Carrier</td><td style="padding:4px 8px">FedEx</td>:::social
The :::social directive auto-detects platforms from link URL hostnames and
produces <mj-social> with <mj-social-element> children. Each element's
name attribute is set to the detected platform, which MJML uses to select
the appropriate icon.
Supported platforms: facebook, twitter, x, instagram, linkedin, youtube,
github, pinterest, dribbble, medium, snapchat, soundcloud, tumblr, vimeo,
xing. Unknown URLs produce name="web".
---
subject: Test
---
::: social
[Follow us](https://twitter.com/example)
:::
.
<mj-social-element name="twitter" href="https://twitter.com/example">Follow us</mj-social-element>Unknown URLs produce the generic web icon:
---
subject: Test
---
::: social
[Visit](https://example.com)
:::
.
<mj-social-element name="web" href="https://example.com">Visit</mj-social-element>X (formerly Twitter) detection:
---
subject: Test
---
::: social
[Follow](https://x.com/example)
:::
.
name="x"The social section wraps elements in <mj-social> with configurable icon
size (default 20px), mode (default horizontal), and alignment (default
center):
---
subject: Test
---
::: social
[GitHub](https://github.com/example)
:::
.
<mj-social font-size="0px" icon-size="20px" mode="horizontal" align="center":::centered
The :::centered directive wraps content in an <mj-text> with
align="center".
---
subject: Test
---
::: centered
Thank you for your purchase.
:::
.
<mj-text align="center">
.
Thank you for your purchase.:::raw
The :::raw directive passes through HTML content verbatim inside an
<mj-raw> tag. The HTML must be inside a fenced code block within the
directive.
---
subject: Test
---
::: raw
```html
<div class="custom">Raw HTML content</div>
```
:::
.
<mj-raw><div class="custom">Raw HTML content</div></mj-raw>Theme
Theme colors in mj-head
The default theme colors appear throughout the <mj-head> boilerplate.
The brand color is used for link styling and blockquote borders.
---
subject: Test
---
Hello
.
a { color: #18181b; text-decoration: none; }
.
blockquote { border-left: 3px solid #18181b;Frontmatter theme overrides
Frontmatter keys matching theme fields (in camelCase) override the defaults
for that template. For example, brandColor in frontmatter overrides the
brand color used in links, buttons, and highlight backgrounds.
---
subject: Test
brandColor: "#ff0000"
---
Hello
.
a { color: #ff0000; text-decoration: none; }Padding Resolution (MJML-level)
Directives and images use MJML-level padding attributes (not inline CSS). The same named tokens are available but resolve to different default values depending on context.
| Token | Value |
|---|---|
none |
0 |
compact |
10px 20px |
normal |
24px 32px |
spacious |
40px 32px |
Numeric values are converted to pixel strings. Single values ("16" becomes
"16px"), two values ("16 32" becomes "16px 32px"), and four values
("8 16 8 16" becomes "8px 16px 8px 16px") are supported. Three values
and non-numeric values fall back to the context's default.
---
subject: Test
---
{padding="compact"}
.
padding="10px 20px"Numeric two-value padding:
---
subject: Test
---
{padding="16 32"}
.
padding="16px 32px"Section Directive
:::section is shorthand for single-column :::columns + :::col. It compiles
to the same MJML output with no boilerplate.
---
subject: Test
---
::: section {bg="#f9f9f9" padding="compact"}
Hello from a section
:::
.
background-color="#f9f9f9"
.
Hello from a sectionSection with flush attribute:
---
subject: Test
---
::: section {bg="#000" flush}
Dark section content
:::
.
background-color="#000"
.
Dark section content