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 brand.
---
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 brand’s tokens.colors.button, text color from tokens.colors.button-text,
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 brand’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>Heading typography from brand.yaml
Per-heading typography tokens in brand.tokens.typography.heading-N drive
the default heading CSS in the compiled email. Tokens that are present
override the compiler’s built-in defaults (32 / 24 / 20 / 18 px, weight
700/700/600/600); tokens that are absent fall back to those defaults.
Supported fields per heading level: fontSize, fontWeight,
lineHeight, letterSpacing (h1 / h2 also accept lineHeight;
letterSpacing is wired for h1 today). Dimension tokens accept px and
rem (§ 5.5; em not yet supported in the brand model).
Inline attribute overrides on individual headings (e.g.
# Headline\n{size="6xl" tracking="-0.025em"}) still win — the cascade
remains inline > {.style} > defaults > inference > brand tokens > built-in.
Example. With this brand.yaml:
tokens:
typography:
body:
fontSize: 16
heading-1:
basedOn: body
fontSize: 48
fontWeight: 500
letterSpacing: "-1.4px"…and this template:
---
subject: Test
---
# Display headline…the compiled <style> block contains:
h1 { font-size: 48px; font-weight: 500; letter-spacing: -1.4px; color: ...; margin: 0 0 12px 0; }The contract is enforced by brand_pipeline_heading_* tests in
crates/core/src/compiler/email_tests.rs.
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
brand’s tokens.colors.button as background and tokens.colors.button-text 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 brand’s secondary color. The text uses tokens.colors.secondary-text.
---
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 brand’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 brand’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 brand’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"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 brand. 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, or accepts a
raw pixel value (size="44" or size="44px") for sizes between named
tokens.
| Token | Value |
|---|---|
xs |
12px |
sm |
14px |
md |
16px |
lg |
18px |
xl |
20px |
2xl |
24px |
3xl |
30px |
4xl |
36px |
5xl |
48px |
6xl |
60px |
7xl |
72px |
8xl |
96px |
---
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"---
subject: Test
---
Display heading
{size="5xl"}
.
style="font-size:48px"---
subject: Test
---
Custom hero size
{size="44px"}
.
style="font-size:44px"Tracking (letter-spacing)
The tracking attribute emits CSS letter-spacing on the text block.
Accepts named tokens or a raw em / px / rem value.
| Token | letter-spacing |
|---|---|
tighter |
-0.05em |
tight |
-0.025em |
normal |
0 |
wide |
0.025em |
wider |
0.05em |
---
subject: Test
---
Display headline
{size="5xl" tracking="tight"}
.
letter-spacing:-0.025em---
subject: Test
---
Display headline
{size="5xl" tracking="-0.025em"}
.
letter-spacing:-0.025emColor tokens
color resolves in two tiers:
- Literal
#hex— passed through. - Any name defined under
brand.tokens.colors— looked up and replaced with the literal hex.
Unknown values MUST fail with template/unknown-color-token (severity Error). Implementations MUST NOT silently render an empty value.
The compiled-in default brand defines (among others):
| Token name | Default value |
|---|---|
brand |
#18181b |
heading |
#09090b |
body |
#3f3f46 |
secondary |
#e4e4e7 |
secondary-text |
#71717a |
danger |
#ef4444 |
success |
#22c55e |
warning |
#f59e0b |
A custom brand.yaml shadows these and adds its own.
---
subject: Test
---
Subdued text here.
{color="secondary-text"}
.
<p style="color:#71717a">Subdued text here.</p>---
subject: Test
---
Error message
{color="danger"}
.
color:#ef4444Background tokens
bg resolves identically to color — #hex first, then any name in brand.tokens.colors. Unknown values MUST fire template/unknown-bg-token (Error).
Common brand tokens used as backgrounds:
| Token name | Default value |
|---|---|
brand |
#18181b |
card |
#f4f4f5 |
warning |
#f59e0b |
success |
#22c55e |
danger |
#ef4444 |
Any other token defined in brand.tokens.colors is also valid.
---
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 brand’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
brand’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"Background images on :::columns and :::col
Both :::columns (section level) and :::col (column level) accept
bg-image, bg-size, bg-repeat, and bg-position attributes.
On :::columns, the attributes map to background-url, background-size,
background-repeat, and background-position on <mj-section>.
---
subject: Test
---
::: columns {bg-image="https://cdn.example.com/bg.jpg" bg-size="cover" bg-repeat="no-repeat" bg-position="center top"}
::: col
Content over background.
:::
:::
.
background-url="https://cdn.example.com/bg.jpg"
.
background-size="cover"
.
background-repeat="no-repeat"
.
background-position="center top"On :::col, the same attributes map to the corresponding MJML attributes on
<mj-column>. The compiled MJML is valid, but note that mrml does not yet
render background-url on <mj-column> to HTML — the background will not
appear in the final email until mrml adds support.
---
subject: Test
---
::: columns
::: col {bg-image="https://cdn.example.com/col-bg.jpg"}
Column with background image.
:::
::: col
Normal column.
:::
:::
.
background-url="https://cdn.example.com/col-bg.jpg"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 brand’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 brand’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. Each row is
tagged with norq-fields-key / norq-fields-value classes; the email head
ships a @media (max-width: 480px) rule that stacks the cells vertically on
mobile viewports.
---
subject: Test
---
::: fields
Status: Shipped
Carrier: FedEx
:::
.
<td class="norq-fields-key" style="font-weight:bold;padding:4px 8px">Status</td><td class="norq-fields-value" style="padding:4px 8px">Shipped</td>
.
<td class="norq-fields-key" style="font-weight:bold;padding:4px 8px">Carrier</td><td class="norq-fields-value" 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>Brand
Brand colors in mj-head
The default brand 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 brand overrides
A brand: block in frontmatter overrides specific brand tokens for one
template. The shape mirrors brand.yaml itself — e.g. setting
brand.colors.brand overrides the brand color used in links, buttons, and
highlight backgrounds for that single notification.
---
subject: Test
brand:
colors:
brand: "#ff0000"
---
Hello
.
a { color: #ff0000; text-decoration: none; }Content width
The email canvas defaults to 600px wide. Override per-project via
email.contentWidth in norq.config.yaml, or per-template via
contentWidth at frontmatter root. The override accepts either a unit-
suffixed string ("720px") or a bare number (treated as pixels).
# norq.config.yaml
email:
contentWidth: "720px"# email.md frontmatter — wins over the project-wide setting
---
subject: Test
contentWidth: "720px"
---The resolved value MUST appear as width="<n>px" on the rendered MJML
container.
Color mode (colorMode: auto)
Email is the only channel that compiles dark-mode styles. Setting
brand.modes.colorMode: auto (in brand.yaml) instructs the compiler to:
- Emit
<meta name="color-scheme" content="light dark">in<head>. - Generate an
@media (prefers-color-scheme: dark)block whose body re-binds the resolved color tokens (brand,heading,body,background,content,card,button,secondary,danger,success,warning, plus their*-textcompanions and any custom tokens that have adark.colors.<name>override). - Apply the same dark colors to inline
<a>link styling and blockquote borders inside the dark media block.
Other channels (SMS, Slack, push, WhatsApp, MS Teams) MUST ignore
colorMode and always render light values.
The other valid colorMode values are light (default — never emit dark
CSS) and dark (always render with the dark token set, no media query).
Custom HTTP headers (headers:)
Frontmatter MAY include a headers: block whose key-value pairs are passed
through verbatim as custom email headers (e.g. X-Campaign-Id,
List-Unsubscribe). Values are template-interpolated against runtime data.
Providers that support custom headers (Resend, SendGrid) MUST forward them;
others MAY drop unknown headers but MUST NOT error.
---
subject: "Order shipped"
headers:
X-Campaign-Id: "{{campaign.id}}"
X-Notification-Type: "transactional"
---The compiler emits these on the EmailOutput.headers field; they are not
inlined into the HTML.
Right-to-left layout (dir/lang)
dir and lang frontmatter keys MUST be emitted on the rendered <html>
element. dir: rtl flips text direction across the entire email; lang
takes any BCP-47 language tag.
---
subject: "..."
dir: rtl
lang: ar
---<html lang="ar" dir="rtl" ...>dir defaults to auto and lang defaults to und when neither is set.
Iterable tables (:::table)
The :::table directive renders a row-iterable table. The directive header
binds an array path to a per-row variable (:::table items as item); the
markdown table inside the directive body declares the column headers and a
single row template. Each runtime row produces one rendered table row.
::: table cart as item
| Product | Qty | Total |
|---------|-----|-------|
| {{item.name}} | {{item.qty}} | {{item.total}} |
:::(The compiler tests in crates/core/src/compiler/email_tests.rs exercise the rendered HTML against runtime data — there’s no inline conformance block here because the spec runner doesn’t bind data.)
:::table MUST be inside a directive that supports tables (:::section,
:::col, or top-level). Other channels handle :::table as follows:
- SMS — silently ignored (no tabular layout in plaintext).
- Slack — silently ignored (Slack Block Kit has no native iterable
table; authors should use a
:::listor repeated:::sectionblocks instead). - WhatsApp — silently ignored.
- Microsoft Teams — rendered as a FactSet (one fact per row) when appropriate; otherwise ignored.
- Push — ignored (push has no body-content directives).
Pre-existing :::table content compiles per the conformance examples in
syntax.md; this section only normalises the email-channel
output.
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