Norq

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 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: ![alt](url). 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
---
 
![Product photo](https://example.com/product.jpg)
.
<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
---
 
![](https://example.com/hero.jpg)
.
<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
---
 
![Banner](https://example.com/banner.jpg){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
---
 
![Icon](https://example.com/icon.png){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
---
 
![Logo](https://example.com/logo.png){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
---
 
![Hero](https://example.com/hero.jpg){padding="0"}
.
<mj-image src="https://example.com/hero.jpg" alt="Hero" padding="0px" border="none" />

Named tokens resolve to pixel values:

---
subject: Test
---
 
![Photo](https://example.com/photo.jpg){padding="spacious"}
.
padding="40px 32px"

Image border radius

The {border-radius="N"} attribute rounds the image corners.

---
subject: Test
---
 
![Avatar](https://example.com/avatar.jpg){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 — [![alt](image-url)](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
---
 
[![Shop now](https://example.com/banner.jpg)](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
---
 
[![Logo](https://example.com/logo.png)](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
---
 
![Hero](https://example.com/hero.jpg)
.
<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 ![icon](https://example.com/icon.png) inline icon.
.
<img
.
icon

Buttons

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.025em

Color tokens

color resolves in two tiers:

  1. Literal #hex — passed through.
  2. 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:#ef4444

Background 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:#f4f4f5

Weight 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 content

Column 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>

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

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:

  1. Emit <meta name="color-scheme" content="light dark"> in <head>.
  2. 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 *-text companions and any custom tokens that have a dark.colors.<name> override).
  3. 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 :::list or repeated :::section blocks 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
---
 
![Photo](https://example.com/photo.jpg){padding="compact"}
.
padding="10px 20px"

Numeric two-value padding:

---
subject: Test
---
 
![Photo](https://example.com/photo.jpg){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 section

Section with flush attribute:

---
subject: Test
---
 
::: section {bg="#000" flush}
Dark section content
:::
.
background-color="#000"
.
Dark section content