Template Directives
Directives use :::name ... ::: syntax to mark semantic regions of your template (similar to fenced code blocks in Markdown). Each directive compiles to the appropriate structure for each channel.
Norq has 17 directives: 14 content directives, 2 control flow directives, and :::table for iterable data tables.
Directive parameters
Content directives accept optional {key="value"} attributes on the opening line. Each directive documents the attributes it honours and which channels apply them — most are email-only but some are read by other channels (e.g. :::media {type="video"} is read by the Slack compiler). Expressions like {{var}} are supported in values.
Universal attributes
All directives support:
| Attribute | Values | Effect |
|---|---|---|
align |
left, center, right |
Text alignment |
bg |
#hex, or any name from brand.tokens.colors (e.g. brand, card, secondary, warning) |
Background color |
color |
#hex, or any name from brand.tokens.colors (e.g. brand, body, secondary-text, success) |
Text color |
padding |
none, compact, normal, spacious, or numeric ("16", "16 32") |
Vertical padding |
size |
xs, sm, md, lg, xl, 2xl, 3xl, 4xl |
Font size |
weight |
light, normal, medium, semibold, bold |
Font weight |
spacing |
tight, normal, relaxed, loose, or numeric 0.5–3.0 |
Line height |
bg and color resolve in two tiers: literal #hex first, then any token name defined in brand.tokens.colors (so bg="secondary" resolves to brand.tokens.colors.secondary if you’ve defined it). Values that match neither tier fail with template/unknown-bg-token / template/unknown-color-token at lint time (Error severity) — typos can’t silently pass through.
Editor metadata attributes ($-prefixed)
Attribute keys starting with $ are reserved for editor and tooling metadata. They are preserved on disk (parser + norq fmt round-trip them byte-exact) but ignored by every channel compiler — they never appear in rendered output.
| Attribute | Context | Values | Effect |
|---|---|---|---|
$editable-by |
Block-only | Any string (role name — consumer-defined) | Editor-enforced RBAC: only users matching the role can edit this block. Does not affect rendering. |
Unregistered $-prefixed attributes (e.g., $customTag="v1") are silently accepted by the linter so editors and vendor tooling can store arbitrary metadata without a spec PR. Vendor-specific metadata should be further namespaced ($suprsend-*, $maily-*).
::: section {$editable-by="admin"}
Legal footer — edited by compliance team only.
:::Placing a registered block-only attribute (like $editable-by) on an inline element fires editor/metadata-wrong-context:
<!-- ❌ $editable-by is not valid on inline links -->
See [our docs](https://example.com){$editable-by="admin"}.
<!-- ✅ Lock the surrounding block instead -->
::: section {$editable-by="admin"}
See [our docs](https://example.com).
:::Directive-only attributes
| Attribute | Directive | Values |
|---|---|---|
url |
:::hero |
URL string for the hero image |
width |
:::col |
1/2, 1/3, 2/3, 1/4, 3/4, pixels, or percentage |
valign |
:::col |
top, middle, bottom |
variant |
:::action |
primary, secondary, danger, success, warning |
bg-image |
:::columns |
URL string for section background image |
bg-size |
:::columns |
CSS background-size (default "cover") |
bg-repeat |
:::columns |
CSS background-repeat (default "no-repeat") |
bg-position |
:::columns |
CSS background-position (default "center center") |
flush |
:::columns, :::section |
Fuse adjacent sections into <mj-wrapper> (no value needed) |
mobile |
:::columns, :::section |
"inline" prevents mobile stacking (emits <mj-group>) |
mode |
:::social |
horizontal (default), vertical |
Examples:
::: header {bg="#1e3a5f" color="#ffffff" align="center"}
White text on dark header
:::
::: callout {bg="#fff3cd" color="#856d0e"}
Warning-styled callout
:::
::: footer {align="right" color="#999999"}
[Unsubscribe]({{unsubscribe_url}})
:::
::: hero {url="{{hero_image_url}}"}
# Dynamic background image
:::Content directives
:::header
Top section of the notification – logos, brand name, title.
::: header
Order Shipped
:::| Channel | Compilation target |
|---|---|
| Background-colored MJML section with text | |
| SMS | Ignored |
| Slack | Block Kit header block (plain_text) |
| Push | Title source (highest priority) |
Bold text (*header text*) |
|
| Teams | TextBlock with ExtraLarge + Bolder |
:::footer
Bottom section – legal text, unsubscribe links, context.
::: footer
[Unsubscribe]({{unsubscribe_url}})
:::| Channel | Compilation target |
|---|---|
| Small-font MJML section (12px, #666) | |
| SMS | Appended after newline |
| Slack | Context block (mrkdwn) |
| Push | Ignored |
| Footer text in interactive messages; ignored in text-only | |
| Teams | TextBlock with isSubtle=true, size=Small |
:::table
Iterable data table — combines table rendering with array iteration. Header row renders once, template row repeats per item. Supports variable pipe arguments in cells.
::: table products as item
| Product | Qty | Subtotal |
|---------|-----|----------|
| {{item.name}} | {{item.qty}} | {{item.cost | multiply item.qty | currency "USD"}} |
:::| Channel | Compilation target |
|---|---|
HTML <table> with styled header and data rows |
|
| SMS | Pipe-separated text (Product | Qty | Subtotal) |
| Slack | Bold headers + pipe-separated rows in a section |
| Push | Pipe-separated text |
| Pipe-separated text | |
| Teams | Adaptive Card FactSet |
If the collection is empty, only the header row renders (empty table). The | inside {{ }} expressions is handled correctly — it won’t break column separation.
:::action
CTA buttons. Contains Markdown links with style hints.
::: action
[Track Order]({{tracking_url}}){primary}
[Cancel Order]({{cancel_url}}){danger}
[View Details]({{details_url}}){secondary}
:::Button styles: {primary}, {secondary}, {danger}, {success}, {warning}.
| Channel | Compilation target |
|---|---|
mj-button with href |
|
| SMS | ”Label: url” plain text |
| Slack | Actions block with button elements (style=primary) |
| Push | action_url extracted from first link |
| Reply buttons (max 3) in interactive mode; URL buttons in template mode | |
| Teams | Action.OpenUrl entries in card actions array |
:::callout
Highlighted note or warning box.
::: callout
Your payment method will be charged within 24 hours.
:::| Channel | Compilation target |
|---|---|
| Background-colored MJML section (#fff3cd) | |
| SMS | Rendered as plain text (no styling) |
| Slack | Section block with :information_source: prefix |
| Push | Body text with warning symbol prefix |
| Body text with warning symbol prefix | |
| Teams | Container with style=accent |
:::hero
Full-width hero image.
::: hero

:::| Channel | Compilation target |
|---|---|
| Content rendered inline (images become mj-image) | |
| SMS | Ignored |
| Slack | Content rendered as normal blocks |
| Push | Image URL extracted for push image field |
| Image URL extracted; triggers image message type | |
| Teams | Image element with size=Stretch |
:::fields
Key-value pairs, one per line. Useful for order details, invoice line items, etc.
::: fields
Order ID: {{order.id}}
Status: {{order.status}}
Total: {{order.total}}
:::| Channel | Compilation target |
|---|---|
| HTML table with bold keys (font-weight:bold) | |
| SMS | Plain text lines (“Key: Value”) |
| Slack | Section block with fields array (mrkdwn: *Key:* Value) |
| Push | Body text lines (“Key: Value”) |
Bold key formatting (*Key:* Value) |
|
| Teams | FactSet with title/value facts |
:::media
Embedded image, video, or file. Use the type parameter to specify the media type.
::: media type=image

:::
::: media type=video
[Watch the demo](https://cdn.example.com/demo.mp4)
:::Params: type=image (default), type=video, type=file.
| Channel | Compilation target |
|---|---|
| mj-image for images; link for other types | |
| SMS | URL as plain text; ignored for non-URL content |
| Slack | Image block for images; mrkdwn link for others |
| Push | Rendered as text (graceful degradation) |
| Ignored (hero/images handled separately) | |
| Teams | Image element; altText from alt attribute |
:::columns
Multi-column layout with nested :::col blocks.
::: columns
::: col
**Shipping Address**
123 Main St
New York, NY 10001
:::
::: col
**Billing Address**
456 Oak Ave
San Francisco, CA 94102
:::
::::::col uses {key="value"} syntax for width, vertical alignment, and background color:
::: columns
::: col {width="2/3" valign="top"}
Main content with more horizontal space.
:::
::: col {width="1/3" bg="#f4f4f5"}
Sidebar content.
:::
:::| Attribute | Values | Effect |
|---|---|---|
width |
"1/2", "1/3", "2/3", "1/4", "3/4", "200px", "50%" |
Column width as fraction, pixels, or percentage (email only) |
valign |
"top", "middle", "bottom" |
Vertical alignment (email only) |
bg |
"#hex" |
Column background color (email only) |
Without width, columns split equally. Width fractions map to MJML percentage widths. Pixel values ("200px") and percentage values ("50%") are also accepted.
:::columns supports background image attributes on the opening line:
| Attribute | Default | Effect |
|---|---|---|
bg-image |
— | URL for section background image |
bg-size |
"cover" |
CSS background-size |
bg-repeat |
"no-repeat" |
CSS background-repeat |
bg-position |
"center top" |
CSS background-position |
::: columns {bg-image="https://cdn.example.com/bg.jpg" bg-size="cover"}
::: col
Content over background image.
:::
:::Flush sections ({flush})
Add {flush} to fuse consecutive :::columns (or :::section) blocks into a single <mj-wrapper>. This eliminates gaps between stacked sections – useful for hero-image-over-content patterns:
::: columns {flush bg="#1e3a5f"}
::: col

:::
:::
::: columns {flush}
::: col
Content directly below the banner, no gap.
:::
:::Consecutive {flush} sections merge into one <mj-wrapper>. The wrapper background color comes from the first section’s bg. Images inside flush sections default to padding="0" (compiler inference).
Mobile stacking ({mobile="inline"})
By default, columns stack vertically on mobile. Use {mobile="inline"} to prevent stacking – columns stay side-by-side by wrapping them in <mj-group>:
::: columns {mobile="inline"}
::: col {width="1/4"}

:::
::: col {width="3/4"}
Text that stays beside the icon on mobile.
:::
:::Block-level attributes can also be applied to content inside columns using {key="value"} syntax. See the email channel page for details.
| Channel | Compilation target |
|---|---|
| MJML native multi-column layout (mj-section > mj-column) | |
| SMS | Stacked sequentially as text |
| Slack | Section block with fields array (one field per column) |
| Push | Stacked as text (graceful degradation) |
| Stacked as text lines | |
| Teams | ColumnSet with Column elements (width=stretch) |
:::section
Single-column layout shorthand. :::section is syntactic sugar for :::columns + :::col with one column. Use it when you need section-level attributes (background color, flush, padding) without multi-column layout.
::: section {bg="#f4f4f5" padding="spacious"}
Content with a background color, in a single column.
:::Equivalent to:
::: columns {bg="#f4f4f5" padding="spacious"}
::: col
Content with a background color, in a single column.
:::
::::::section accepts all :::columns attributes: bg, padding, flush, mobile, bg-image, etc. It is transformed to :::columns + :::col at parse time, so all channel compilers handle it transparently.
| Channel | Compilation target |
|---|---|
mj-section > mj-column (single column) |
|
| SMS | Stacked as text |
| Slack | Section block |
| Push | Stacked as text |
| Stacked as text | |
| Teams | Container element |
:::list
Styled list.
::: list
- Choose your plan
- Add your team
- Start sending
:::| Channel | Compilation target |
|---|---|
| HTML list inside mj-text | |
| SMS | Numbered lines |
| Slack | Section block with list-formatted mrkdwn |
| Push | Text lines |
| Text lines | |
| Teams | TextBlock with list text |
:::raw
Raw channel-native code. An escape hatch for channel-specific features that directives don’t cover.
::: raw
```html
<table style="width:100%">
<tr><td>Custom HTML</td></tr>
</table>
```
:::The content must be a fenced code block inside the directive.
| Channel | Compilation target |
|---|---|
| mj-raw passthrough (HTML inside code block) | |
| SMS | Ignored |
| Slack | Parsed JSON inserted as Block Kit block(s) |
| Push | Ignored |
| Ignored | |
| Teams | Parsed JSON inserted as Adaptive Card element |
:::highlight
Dark card with prominent text. Use for announcements, special offers, or key information that must stand out.
::: highlight
Limited-time offer: 50% off all plans!
:::| Channel | Compilation target |
|---|---|
Dark mj-column (brand-color background, white text, 600 weight, 8px radius) |
|
| SMS | Star prefix (⭐ text) |
| Slack | section block with bold mrkdwn (*text*) |
| Push | Star prefix (⭐ text) |
Bold text (*text*) |
|
| Teams | Container with style=attention and TextBlock |
:::centered
Center-aligned text block. Use for taglines, thank-you messages, or any content that reads better centered.
::: centered
Thank you for being a customer.
:::| Channel | Compilation target |
|---|---|
mj-text with align="center" |
|
| SMS | Plain text (no alignment in SMS) |
| Slack | Plain text (no alignment in Slack) |
| Push | Plain text (no alignment in push) |
| Plain text (no alignment in WhatsApp) | |
| Teams | Plain text (no alignment in Adaptive Cards) |
:::social
Social media icon links. Platforms are auto-detected from link URL hostnames: Facebook, X/Twitter, Instagram, LinkedIn, YouTube, GitHub, Pinterest, Dribbble, Medium, Snapchat, SoundCloud, Tumblr, Vimeo, Xing. Unknown URLs get a generic “web” icon.
::: social
[Facebook](https://facebook.com/yourpage)
[GitHub](https://github.com/yourrepo)
[LinkedIn](https://linkedin.com/company/yourco)
:::Attributes: mode ("horizontal" default, "vertical"), align, size, padding.
::: social {mode="vertical" align="center"}
[Instagram](https://instagram.com/yourprofile)
[YouTube](https://youtube.com/yourchannel)
:::| Channel | Compilation target |
|---|---|
mj-social with auto-detected platform icons |
|
| SMS | Text links or ignored |
| Slack | Text links or ignored |
| Push | Ignored |
| Text links or ignored | |
| Teams | Text links or ignored |
Divider attributes
Thematic breaks (***) support block attributes for customizing the divider appearance:
***
{width="50%" color="#e4e4e7" thickness="2"}| Attribute | Default | Effect |
|---|---|---|
width |
100% |
Divider width |
color |
— | Border color (supports brand tokens or #hex) |
thickness |
— | Border width in pixels |
Control flow directives
:::if / :::else
Conditional rendering. See the control flow page for full details.
::: if order.shipped
Your order has shipped!
:::else
Your order is being processed.
::::::each
Loop over an array. See the control flow page for full details.
::: each order.items as item
- {{item.name}} x {{item.qty}} -- ${{item.price}}
:::