Norq

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.53.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

Top section of the notification – logos, brand name, title.

::: header
Order Shipped
:::
Channel Compilation target
Email Background-colored MJML section with text
SMS Ignored
Slack Block Kit header block (plain_text)
Push Title source (highest priority)
WhatsApp Bold text (*header text*)
Teams TextBlock with ExtraLarge + Bolder

Bottom section – legal text, unsubscribe links, context.

::: footer
[Unsubscribe]({{unsubscribe_url}})
:::
Channel Compilation target
Email Small-font MJML section (12px, #666)
SMS Appended after newline
Slack Context block (mrkdwn)
Push Ignored
WhatsApp 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
Email 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
WhatsApp 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
Email mj-button with href
SMS ”Label: url” plain text
Slack Actions block with button elements (style=primary)
Push action_url extracted from first link
WhatsApp 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
Email 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
WhatsApp Body text with warning symbol prefix
Teams Container with style=accent

:::hero

Full-width hero image.

::: hero
![Hero banner](https://cdn.example.com/hero.png)
:::
Channel Compilation target
Email Content rendered inline (images become mj-image)
SMS Ignored
Slack Content rendered as normal blocks
Push Image URL extracted for push image field
WhatsApp 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
Email 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”)
WhatsApp 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
![Product photo](https://cdn.example.com/product.jpg)
:::
 
::: media type=video
[Watch the demo](https://cdn.example.com/demo.mp4)
:::

Params: type=image (default), type=video, type=file.

Channel Compilation target
Email 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)
WhatsApp 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
![Banner](https://cdn.example.com/hero.png)
:::
:::
 
::: 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"}
![Icon](https://cdn.example.com/icon.png)
:::
::: 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
Email 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)
WhatsApp 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
Email mj-section > mj-column (single column)
SMS Stacked as text
Slack Section block
Push Stacked as text
WhatsApp Stacked as text
Teams Container element

:::list

Styled list.

::: list
- Choose your plan
- Add your team
- Start sending
:::
Channel Compilation target
Email HTML list inside mj-text
SMS Numbered lines
Slack Section block with list-formatted mrkdwn
Push Text lines
WhatsApp 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
Email mj-raw passthrough (HTML inside code block)
SMS Ignored
Slack Parsed JSON inserted as Block Kit block(s)
Push Ignored
WhatsApp 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
Email 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)
WhatsApp 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
Email 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)
WhatsApp 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
Email mj-social with auto-detected platform icons
SMS Text links or ignored
Slack Text links or ignored
Push Ignored
WhatsApp 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}}
:::