assets/examples/basic-layout.mjml
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>Basic layout example</mj-title>
</mj-head>
<mj-body>
<!-- Company Header -->
<mj-section background-color="#f0f0f0">
<mj-column>
<mj-text
align="center"
font-style="italic"
font-size="20px"
color="#626262"
>
My Company
</mj-text>
</mj-column>
</mj-section>
<!-- Image Header -->
<mj-section
background-url="https://1.bp.blogspot.com/-TPrfhxbYpDY/Uh3Refzk02I/AAAAAAAALw8/5sUJ0UUGYuw/s1600/New+York+in+The+1960's+-+70's+(2).jpg"
background-size="cover"
background-repeat="no-repeat"
>
<mj-column width="600px">
<mj-text
align="center"
color="#fff"
font-size="40px"
font-family="Helvetica Neue, Helvetica, Arial, sans-serif"
>
Slogan here
</mj-text>
<mj-button background-color="#F63A4D" href="#">
Promotion
</mj-button>
</mj-column>
</mj-section>
<!-- Introduction Text -->
<mj-section background-color="#fafafa">
<mj-column width="400px">
<mj-text
font-style="italic"
font-size="20px"
font-family="Helvetica Neue, Helvetica, Arial, sans-serif"
color="#626262"
>
My Awesome Text
</mj-text>
<mj-text color="#525252">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim
eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras
id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh.
Proin nec commodo purus. Sed eget nulla elit. Nulla aliquet mollis
faucibus.
</mj-text>
<mj-button background-color="#F45E43" href="#">Learn more</mj-button>
</mj-column>
</mj-section>
<!-- 2 Columns Section -->
<mj-section background-color="white">
<!-- Left image -->
<mj-column>
<mj-image
alt="Black and white photograph of a Paris street scene"
width="200px"
src="https://designspell.files.wordpress.com/2012/01/sciolino-paris-bw.jpg"
/>
</mj-column>
<!-- Right paragraph -->
<mj-column>
<mj-text
font-style="italic"
font-size="20px"
font-family="Helvetica Neue, Helvetica, Arial, sans-serif"
color="#626262"
>
Find amazing places
</mj-text>
<mj-text color="#525252">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim
eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras
id dui lectus. Vestibulum sed finibus lectus.
</mj-text>
</mj-column>
</mj-section>
<!-- Icons -->
<mj-section background-color="#fbfbfb">
<mj-column>
<mj-image
alt="Decorative icon"
padding="10px"
width="100px"
src="https://191n.mj.am/img/191n/3s/x0l.png"
/>
</mj-column>
<mj-column>
<mj-image
alt="Decorative icon"
padding="10px"
width="100px"
src="https://191n.mj.am/img/191n/3s/x01.png"
/>
</mj-column>
<mj-column>
<mj-image
alt="Decorative icon"
padding="10px"
width="100px"
src="https://191n.mj.am/img/191n/3s/x0s.png"
/>
</mj-column>
</mj-section>
<!-- Social icons -->
<mj-section background-color="#e7e7e7">
<mj-column>
<mj-social>
<mj-social-element name="facebook">Share</mj-social-element>
</mj-social>
</mj-column>
</mj-section>
</mj-body>
</mjml>
assets/examples/newsletter.mjml
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>The Workbench — Issue 42: desk ergonomics that actually stick</mj-title>
<mj-preview>Issue 42: the three ergonomics changes that survive a busy week.</mj-preview>
<mj-raw>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
</mj-raw>
<mj-font name="Inter" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700" />
<mj-attributes>
<mj-all font-family="Inter, Arial, sans-serif" />
<mj-text color="#2b2b2b" font-size="15px" line-height="24px" padding="8px 24px" />
<mj-divider border-color="#e3e6ea" border-width="1px" border-style="solid" padding="16px 24px" />
<mj-class name="h1" font-size="26px" line-height="34px" font-weight="700" color="#121212" />
<mj-class name="h2" font-size="19px" line-height="26px" font-weight="700" color="#121212" />
<mj-class name="meta" font-size="12px" line-height="18px" color="#5a5a5a" />
</mj-attributes>
<mj-html-attributes>
<mj-selector path=".email-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">1</mj-html-attribute>
</mj-selector>
<mj-selector path=".story-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">2</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
<!-- Light logo visible by default; dark logo hidden -->
<mj-style inline="inline">
.dark-logo { display: none !important; }
</mj-style>
<!-- Dark mode overrides -->
<mj-style>
@media (prefers-color-scheme: dark) {
.light-logo { display: none !important; }
.dark-logo { display: block !important; }
.dm-bg { background-color: #121212 !important; }
.dm-card { background-color: #1e1e1e !important; }
.dm-text div { color: #F1F1F1 !important; }
.dm-muted div { color: #b9b9b9 !important; }
}
</mj-style>
</mj-head>
<mj-body background-color="#f1f1f1" width="600px" css-class="dm-bg">
<mj-wrapper background-color="#ffffff" padding="24px 0" css-class="dm-card">
<mj-section padding="0">
<mj-column>
<mj-image css-class="light-logo" src="https://cdn.example.com/workbench-logo-light.png" alt="The Workbench" width="180px" href="https://example.com" padding="0 24px 4px" />
<mj-image css-class="dark-logo" src="https://cdn.example.com/workbench-logo-dark.png" alt="The Workbench" width="180px" href="https://example.com" padding="0 24px 4px" />
<mj-text mj-class="meta" css-class="dm-muted" align="center">Issue 42 · 10 August 2026</mj-text>
<mj-divider />
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-text css-class="email-heading dm-text" mj-class="h1">
Desk ergonomics that actually stick
</mj-text>
<mj-text css-class="dm-text">
Most ergonomics advice fails because it asks you to remember something.
The three changes below work because they are physical defaults — once set,
they hold without any effort from you.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column width="35%" vertical-align="top">
<mj-image src="https://cdn.example.com/story-monitor.jpg" alt="A monitor raised to eye level on a wooden riser" width="170px" padding="8px 0 8px 24px" />
</mj-column>
<mj-column width="65%" vertical-align="top">
<mj-text css-class="story-heading dm-text" mj-class="h2">1. Raise the monitor, not the chair</mj-text>
<mj-text css-class="dm-text">
The top of the screen should sit level with your eyes. Raising the chair
instead just moves the problem to your feet.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column width="35%" vertical-align="top">
<mj-image src="https://cdn.example.com/story-keyboard.jpg" alt="A split keyboard positioned at elbow height" width="170px" padding="8px 0 8px 24px" />
</mj-column>
<mj-column width="65%" vertical-align="top">
<mj-text css-class="story-heading dm-text" mj-class="h2">2. Set the keyboard to elbow height</mj-text>
<mj-text css-class="dm-text">
Forearms parallel to the floor. If the desk cannot go lower, a keyboard
tray costs less than a new desk and fixes the same problem.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-divider />
<mj-text mj-class="meta" css-class="dm-muted">
You are receiving this because you subscribed at example.com.
<a href="https://example.com/unsubscribe" style="color:#5a5a5a;">Unsubscribe</a>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
</mj-body>
</mjml>
assets/examples/order-confirmation.mjml
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>Your Northwind order #{{order.number}} is confirmed</mj-title>
<mj-preview>Order {{order.number}} confirmed — arriving {{order.eta}}. Track it anytime.</mj-preview>
<mj-font name="Inter" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700" />
<mj-attributes>
<mj-all font-family="Inter, Arial, sans-serif" />
<mj-text color="#2b2b2b" font-size="15px" line-height="23px" padding="8px 24px" />
<mj-button background-color="#0b5fff" color="#ffffff" border-radius="4px" inner-padding="14px 32px" font-size="15px" font-weight="600" />
<mj-divider border-color="#e3e6ea" border-width="1px" border-style="solid" padding="16px 24px" />
<mj-table font-size="14px" line-height="20px" color="#2b2b2b" padding="8px 24px" />
<mj-class name="h1" font-size="26px" line-height="32px" font-weight="700" color="#121212" />
<mj-class name="muted" font-size="13px" line-height="19px" color="#5a5a5a" />
</mj-attributes>
<mj-html-attributes>
<mj-selector path=".email-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">1</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
<mj-style inline="inline">
.receipt-total td { font-weight: 700; }
</mj-style>
</mj-head>
<mj-body background-color="#f1f1f1" width="600px">
<mj-section background-color="#ffffff" padding="24px 0 0">
<mj-column>
<mj-image src="https://cdn.example.com/northwind-logo.png" alt="Northwind Supply Co." width="160px" href="https://example.com" padding="0 24px 8px" />
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" padding="8px 0 0">
<mj-column>
<mj-text css-class="email-heading" mj-class="h1">
Thanks, {{customer.firstName}} — your order is confirmed
</mj-text>
<mj-text>
Order <strong>#{{order.number}}</strong> was placed on {{order.placedAt}} and is
estimated to arrive by <strong>{{order.eta}}</strong>.
</mj-text>
<mj-divider />
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" padding="0">
<mj-column>
<mj-table>
<tr style="border-bottom:1px solid #e3e6ea;text-align:left;">
<th scope="col" style="padding:0 15px 8px 0;">Item</th>
<th scope="col" style="padding:0 15px 8px;">Qty</th>
<th scope="col" style="padding:0 0 8px 15px;text-align:right;">Price</th>
</tr>
<tr>
<td style="padding:10px 15px 0 0;">Aeron Task Chair — Graphite</td>
<td style="padding:10px 15px 0;">1</td>
<td style="padding:10px 0 0 15px;text-align:right;">$1,195.00</td>
</tr>
<tr>
<td style="padding:10px 15px 0 0;">Monitor Arm, Dual</td>
<td style="padding:10px 15px 0;">2</td>
<td style="padding:10px 0 0 15px;text-align:right;">$298.00</td>
</tr>
<tr class="receipt-total" style="border-top:1px solid #e3e6ea;">
<td style="padding:12px 15px 0 0;">Total</td>
<td style="padding:12px 15px 0;"> </td>
<td style="padding:12px 0 0 15px;text-align:right;">{{order.total}}</td>
</tr>
</mj-table>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" padding="8px 0 24px">
<mj-column>
<mj-button href="https://example.com/orders/{{order.number}}">
Track your order
</mj-button>
</mj-column>
</mj-section>
<mj-raw>
<!-- htmlmin:ignore --> {{#if order.giftMessage}} <!-- htmlmin:ignore -->
</mj-raw>
<mj-section background-color="#ffffff" padding="0 0 24px">
<mj-column>
<mj-divider />
<mj-text mj-class="muted" container-background-color="#fff9e6">
Gift message: {{order.giftMessage}}
</mj-text>
</mj-column>
</mj-section>
<mj-raw>
<!-- htmlmin:ignore --> {{/if}} <!-- htmlmin:ignore -->
</mj-raw>
<mj-include path="./partials/footer.mjml" />
</mj-body>
</mjml>
assets/examples/partials/footer.mjml
<mjml>
<mj-body>
<mj-section background-color="#f1f1f1" padding="24px 0">
<mj-column>
<mj-divider border-color="#d8d8d8" border-width="1px" border-style="solid" padding="0 25px 16px" />
<mj-text align="center" color="#5a5a5a" font-size="12px" line-height="18px">
Northwind Supply Co. · 400 Harbor Way, Seattle, WA 98104
</mj-text>
<mj-text align="center" color="#5a5a5a" font-size="12px" line-height="18px">
<a href="https://example.com/unsubscribe" style="color:#5a5a5a;">Unsubscribe</a> ·
<a href="https://example.com/preferences" style="color:#5a5a5a;">Email preferences</a>
</mj-text> </mj-column>
</mj-section>
</mj-body>
</mjml>
assets/examples/promo-sale.mjml
<mjml lang="en" dir="ltr">
<mj-head>
<mj-title>Summer Sale — up to 50% off everything</mj-title>
<mj-preview>48 hours only. Up to 50% off chairs, desks and lighting.</mj-preview>
<mj-font name="Raleway" href="https://fonts.googleapis.com/css?family=Raleway:400,700" />
<mj-breakpoint width="480px" />
<mj-attributes>
<mj-all font-family="Raleway, Arial, sans-serif" />
<mj-text color="#2b2b2b" font-size="15px" line-height="23px" />
<mj-button background-color="#E63946" color="#ffffff" border-radius="4px" inner-padding="14px 32px" font-weight="700" />
<mj-class name="hero-title" color="#ffffff" font-size="36px" line-height="44px" font-weight="700" />
<mj-class name="hero-sub" color="#f1f1f1" font-size="17px" line-height="25px" />
<mj-class name="card-title" color="#121212" font-size="17px" line-height="24px" font-weight="700" />
</mj-attributes>
<mj-html-attributes>
<mj-selector path=".email-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">1</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
</mj-head>
<mj-body background-color="#f1f1f1" width="600px">
<mj-section background-color="#ffffff" padding="8px 0">
<mj-column>
<mj-navbar base-url="https://example.com" hamburger="hamburger" ico-color="#121212">
<mj-navbar-link href="/chairs" color="#121212" font-family="Raleway, Arial, sans-serif">Chairs</mj-navbar-link>
<mj-navbar-link href="/desks" color="#121212" font-family="Raleway, Arial, sans-serif">Desks</mj-navbar-link>
<mj-navbar-link href="/lighting" color="#121212" font-family="Raleway, Arial, sans-serif">Lighting</mj-navbar-link>
</mj-navbar>
</mj-column>
</mj-section>
<mj-hero
mode="fixed-height"
height="420px"
background-url="https://cdn.example.com/summer-hero.jpg"
background-width="600px"
background-height="420px"
background-color="#1a1a2e"
background-position="center center"
inner-padding="32px 24px"
padding="60px 0">
<mj-text css-class="email-heading" mj-class="hero-title" align="center">
Summer Sale — up to 50% off
</mj-text>
<mj-text mj-class="hero-sub" align="center">
48 hours only. Everything in the workspace collection is marked down.
</mj-text>
<mj-button href="https://example.com/sale" align="center">
Shop the sale
</mj-button>
</mj-hero>
<mj-section
background-url="https://cdn.example.com/texture-slate.jpg"
background-color="#2a3448"
background-size="cover"
background-repeat="no-repeat"
background-position="top center"
padding="32px 0">
<mj-column>
<mj-text align="center" color="#ffffff" font-size="20px" line-height="28px" font-weight="700">
Free shipping on every order over $75
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" gutter="16px" padding="24px 0">
<mj-column background-color="#f7f8fa" border-radius="6px" vertical-align="top">
<mj-image src="https://cdn.example.com/product-chair.jpg" alt="Aeron task chair in graphite" width="240px" padding="0 0 12px" />
<mj-text mj-class="card-title" align="center">Task Chairs</mj-text>
<mj-text align="center">From $595 — save 40%</mj-text>
</mj-column>
<mj-column background-color="#f7f8fa" border-radius="6px" vertical-align="top">
<mj-image src="https://cdn.example.com/product-desk.jpg" alt="Standing desk in white oak" width="240px" padding="0 0 12px" />
<mj-text mj-class="card-title" align="center">Standing Desks</mj-text>
<mj-text align="center">From $749 — save 30%</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" padding="0 0 24px">
<mj-group>
<mj-column vertical-align="middle">
<mj-image src="https://cdn.example.com/icon-returns.png" alt="Free returns" width="48px" padding="4px" />
<mj-text align="center" font-size="12px" line-height="16px">Free returns</mj-text>
</mj-column>
<mj-column vertical-align="middle">
<mj-image src="https://cdn.example.com/icon-warranty.png" alt="12 year warranty" width="48px" padding="4px" />
<mj-text align="center" font-size="12px" line-height="16px">12 yr warranty</mj-text>
</mj-column>
<mj-column vertical-align="middle">
<mj-image src="https://cdn.example.com/icon-support.png" alt="Support seven days a week" width="48px" padding="4px" />
<mj-text align="center" font-size="12px" line-height="16px">7-day support</mj-text>
</mj-column>
</mj-group>
</mj-section>
<mj-section background-color="#121212" padding="24px 0">
<mj-column>
<mj-social mode="horizontal" align="center" icon-size="28px" border="1px solid #121212" color="#ffffff" font-size="12px">
<mj-social-element name="instagram-noshare" href="https://instagram.com/northwind" alt="Northwind on Instagram" />
<mj-social-element name="facebook-noshare" href="https://facebook.com/northwind" alt="Northwind on Facebook" />
<mj-social-element name="youtube" href="https://youtube.com/@northwind" alt="Northwind on YouTube" />
</mj-social>
<mj-text align="center" color="#bdbdbd" font-size="12px" line-height="18px">
Northwind Supply Co. · <a href="https://example.com/unsubscribe" style="color:#bdbdbd;">Unsubscribe</a>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
compilation.md
# MJML Compilation Reference
---
## Hard Rules
1. **No global install** — Never run `npm install -g mjml`. If missing, suggest `npm install -D mjml`.
2. **Use npx or relative path** — `npx mjml` or `./node_modules/.bin/mjml`. Never assume global `$PATH`.
3. **Verify source first** — Check that the `.mjml` file exists and contains valid XML before compiling.
---
## Environment Check
```bash
node -v # Must be v20+ (MJML 5 targets Node LTS 20/22/24; 16/18 are unsupported)
cat package.json # Look for "mjml" in dependencies or devDependencies
```
If `mjml` is not in `package.json`: suggest `npm install -D mjml` and wait for user confirmation.
---
## Standard Compilation Command
```bash
npx mjml <source.mjml> -o <output.html> --config.minify=true --config.validationLevel=strict
```
- `--config.minify=true` — **always required**: keeps payload under Gmail's 102KB clip threshold (v5's `cssnano` also minifies CSS, so overall output is smaller than v4's). It does **not** strip whitespace between tags any more — see below
- `--config.validationLevel=strict` — fail fast on syntax errors during development
**If the template uses `<mj-include>`**, minify/validate are not enough — includes are ignored by default in MJML 5:
```bash
npx mjml <source.mjml> -o <output.html> --config.minify=true --config.validationLevel=strict \
--config.allowIncludes true --config.includePath './partials'
```
Without `--config.allowIncludes true`, MJML silently drops the include and logs a warning instead of failing — check compiled output for missing content before assuming the template is correct. `--config.includePath` allowlists the directory includes may resolve from, in addition to the source file's own directory.
**`--config.validationLevel=strict` does NOT catch this.** A dropped include still exits `0`, still writes the `.html`, and still passes strict validation — the only signal is a warning on stderr. Never treat a clean exit code as proof the include landed. Verify explicitly:
```bash
grep -q "<known-string-from-the-partial>" <output.html> || echo "INCLUDE MISSING"
```
Pick a plain-text string for that grep. With `--config.minify=true`, `htmlnano` rewrites HTML entities (`·` becomes the literal `·`), so grepping for entity markup reports a false failure on a build that actually succeeded.
**The two paths resolve from different bases** — this is the usual reason an allowlisted include still gets denied:
| Path | Resolved relative to |
|------|---------------------|
| `path="..."` on `<mj-include>` | the **template file's** directory |
| `--config.includePath` (relative) | the **current working directory** (since MJML 5.2.2) |
Compiling `src/emails/welcome.mjml` from the repo root, with a partial in `common/`, the include is `path="../../common/shared.mjml"` but the flag is `--config.includePath './common'`. Passing an absolute path avoids the ambiguity entirely. A partial outside the template's own directory that is not allowlisted is denied the same silent way.
---
## Output Pathing
Mirror source structure into `/dist`, or output alongside source:
- `src/emails/welcome.mjml` → `dist/emails/welcome.html`
- `emails/welcome.mjml` → `emails/welcome.html`
Ensure the output directory exists before compiling — MJML will not create it.
---
## Error Recovery
1. Parse error output for line/column number
2. Read the source file at that line
3. Fix the syntax issue
4. Re-attempt compilation once
5. **Judge success by the exit code, never by the file.** A failed compile writes no file at all (the 0-byte case does not occur) — but if a previous good build already sits at that output path, it is left fully intact. So a failed recompile can look exactly like a success: the `.html` is present, non-empty, and stale. Check `$?`
**`CssSyntaxError` with templating tags**: a templating tag inside `<mj-style>` **fails the build** under the mandatory `--config.minify=true` — exit `1`, no HTML written:
```
CssSyntaxError: <css input>:2:56: Unknown word brand.darkColor
```
Sanitization is **not** automatic — `sanitizeStyles` defaults to `false`. Enable it:
```bash
npx mjml <source.mjml> -o <output.html> --config.minify=true --sanitizeStyles
```
- `--config.allowMixedSyntax true` and `--config.templateSyntax` do **not** fix this on their own — verified against 5.4.0, both still throw.
- `--sanitizeStyles` protects `{{ }}` and `[[ ]]` only. Liquid `{% %}` still throws; declare it via `--config.templateSyntax` alongside `--sanitizeStyles`.
- A tag inside `<mj-style inline="inline">` cannot be rescued by any flag — Juice mangles it before PostCSS runs. Move it to a component attribute instead.
The reliable answer is to keep templating tags out of CSS entirely: put them in component attributes or `<mj-raw>`, not in `<mj-style>`.
---
## Implementation Standards
| Standard | Rule |
|----------|------|
| Idempotency | Running twice produces identical output — except `mj-navbar`, `mj-accordion` and `mj-carousel`, which emit a fresh random ID each compile. Do not diff their output to detect real changes |
| Clean Up | Remove partial `.html` if compilation fails |
| Logging | Always log the exact CLI command used (user can audit) |
| Version Pinning | New `package.json` → pin MJML to latest stable major (e.g. `^5.4.0`) |
components/advanced.md
# MJML Advanced Components Reference
## mj-hero
Full-width banner section with background image support. Behaves like `mj-section` with a single `mj-column`, but generates VML for Outlook background image support.
Two modes:
- `fixed-height` (**the default**): uses the `height` attribute, which itself defaults to `0px`
- `fluid-height`: expands based on content — **must be set explicitly**
⚠️ **Always set `mode` and `height`.** The default is `fixed-height` with `height="0px"`, so a hero with no `mode` compiles clean at exit `0` and renders as a **zero-height, invisible hero** (`<td ... style="height:0px;" height="0">`). Verified identical in MJML 4 and 5.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| mode | fluid-height / fixed-height | **fixed-height** |
| height | px / % | 0px (required for fixed-height) |
| background-url | URL | null |
| background-color | CSS color | #ffffff |
| background-width | px / % | parent width (mandatory) |
| background-height | px / % | — (mandatory) |
| background-position | CSS keyword | center center |
| border-radius | string | — |
| vertical-align | top/middle/bottom | top |
| inner-background-color | CSS color | — |
| inner-padding | px / % | — |
| padding | px / % | 0px |
**Required attributes**: `background-width` and `background-height` — use the image's exact pixel dimensions. "Required" is a rendering requirement, **not** a validator one: a hero without them compiles clean at exit `0` even under `--config.validationLevel=strict`. The cost is silent Outlook degradation — the generated `<v:image>` loses its `height`, so the hero collapses in Outlook while looking correct everywhere else.
**MJML 5 change**: `inner-padding` now applies consistently across all clients (previously it only affected Outlook's inner table). Re-check hero spacing visually if migrating a v4 template that relied on the old Outlook-only behavior.
**Fallback**: Always set `background-color` for clients that don't support background images.
**Outlook**: Background images in `mj-hero` work in Outlook (VML generated). Use keyword positions only (top, center, bottom) — pixel values ignored by Outlook.
```xml
<!-- Fixed height hero -->
<mj-hero
mode="fixed-height"
height="400px"
background-url="https://cdn.example.com/hero.jpg"
background-width="600px"
background-height="400px"
background-color="#1a1a2e"
padding="80px 0">
<mj-text align="center" color="#ffffff" font-size="36px" font-weight="bold"
font-family="Arial, sans-serif" line-height="44px">
Summer Sale — Up to 50% Off
</mj-text>
<mj-button href="https://example.com/sale" align="center"
background-color="#E63946" color="#ffffff">
Shop the Sale
</mj-button>
</mj-hero>
```
```xml
<!-- Fluid height hero -->
<mj-hero
mode="fluid-height"
background-url="https://cdn.example.com/banner.jpg"
background-width="600px"
background-height="300px"
background-color="#2a3448"
padding="60px 0">
<mj-text align="center" color="#ffffff" font-size="28px">
New Arrivals
</mj-text>
</mj-hero>
```
---
## mj-raw
Outputs raw HTML/text without MJML processing. Use for:
- Template engine tags (Handlebars, Liquid, Jinja)
- Custom HTML not achievable with MJML components
- Content before `<!doctype html>` (with `position="file-start"`)
| Attribute | Accepts | Description |
|-----------|---------|-------------|
| position | "file-start" | Places content before doctype |
**In mj-body**: inserted as-is in the email body.
**In mj-head**: content added at end of HTML `<head>`.
**Outside mj-head/mj-body + position="file-start"**: added before doctype.
```xml
<!-- Protecting template tags from minifier -->
<mj-raw>
<!-- htmlmin:ignore --> {% if user.first_name %} <!-- htmlmin:ignore -->
</mj-raw>
<mj-text>Hello {{ user.first_name }}!</mj-text>
<mj-raw>
{% else %}
</mj-raw>
<mj-text>Hello there!</mj-text>
<mj-raw>
{% endif %}
</mj-raw>
```
**Minify warning**: The `<` character causes parse errors with minify. Wrap in `<!-- htmlmin:ignore -->` tags or use `<`.
---
## mj-include
Includes an external `.mjml`, `.css`, or `.html` file at compile time.
| Attribute | Accepts | Description |
|-----------|---------|-------------|
| path | string | Relative path to file |
| type | css / html | Specify for non-mjml files |
| css-inline | "inline" | Inline the CSS (type="css" only) |
**MJML 5: disabled by default.** `<mj-include>` tags are silently ignored — with just a console warning, no error — unless includes are explicitly enabled. This is a deliberate security change to prevent path traversal / arbitrary file reads via crafted include paths.
Enable includes:
- CLI: `--config.allowIncludes true`
- Node API: `ignoreIncludes: false`
Scope where includes may resolve from (in addition to the source file's own directory):
- CLI: `--config.includePath './partials'` (or a JSON array of paths: `'["../_common","../vendor"]'`)
- Node API: `includePath: ['/project/templates/_common']`
Absolute paths, UNC paths, Windows drive letters, null bytes, and symlinks that escape the allowed directories are rejected.
**Path bases differ**: the `path` attribute resolves relative to the *template file*, while a relative `--config.includePath` resolves relative to the *current working directory*. Denied includes are dropped silently (exit `0`, stderr warning only) — see `compilation.md`.
```xml
<!-- Include MJML partial -->
<mj-include path="./partials/header.mjml" />
<!-- Include CSS into head -->
<mj-include path="./styles/base.css" type="css" />
<mj-include path="./styles/inline.css" type="css" css-inline="inline" />
<!-- Include raw HTML -->
<mj-include path="./partials/footer.html" type="html" />
```
```bash
npx mjml template.mjml -o dist/template.html \
--config.allowIncludes true \
--config.includePath './partials'
```
**Tip**: Wrap partials in `<mjml><mj-body>` tags for preview in editors, then include via path in the main template.
Use `--config.filePath` CLI flag to set a base path for includes independent of the compiled file's location.
components/content.md
# MJML Content Components Reference
## mj-text
Displays styled text. Content can include raw HTML (`<h1>`, `<p>`, `<a>`, `<strong>`, etc.).
| Attribute | Accepts | Default |
|-----------|---------|---------|
| color | CSS color | #000000 |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| font-size | px | 13px |
| font-style | string | — |
| font-weight | string | — |
| line-height | px / % | 1 |
| letter-spacing | px / em | — |
| align | left/right/center/justify | left |
| text-decoration | string | — |
| text-transform | string | — |
| padding | px / % | 10px 25px |
| height | px | — |
| container-background-color | CSS color | — |
**Accessibility**: Do NOT set `role` or `aria-level` directly on `mj-text` — illegal under strict validation. Use `mj-html-attributes` in the head instead:
```xml
<!-- In mj-head -->
<mj-html-attributes>
<mj-selector path=".email-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">1</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
<!-- On the component -->
<mj-text css-class="email-heading" font-family="Arial, sans-serif" font-size="28px"
font-weight="bold" color="#1a1a1a">
Welcome to Our Newsletter
</mj-text>
```
**Ending tag**: Content is not processed by MJML. Don't use MJML components inside mj-text. Escape `<` as `<` if needed.
---
## mj-image
Responsive image. If no width set, uses parent column width.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| src | URL | — |
| alt | string | '' |
| width | px | parent width |
| height | px | auto |
| max-height | px / % | — |
| href | URL | — |
| target | string | _blank |
| fluid-on-mobile | boolean | false |
| border | CSS border | 0 |
| border-radius | px / % | — |
| align | left/center/right | center |
| padding | px / % | 10px 25px |
| title | string | — |
**Required**: Always include `alt` for accessibility. Alt text is shown when images are disabled.
**Image format**: Always use PNG, JPG, or WebP. SVG is not supported in Outlook and unreliable in other clients.
**fluid-on-mobile**: Set to `true` to force full viewport width on mobile even if `width` is set.
```xml
<mj-image src="https://cdn.example.com/logo.png" alt="Company Logo"
width="200px" href="https://example.com" />
```
---
## mj-button
A cross-client compatible CTA button (uses HTML table internally, not CSS button).
| Attribute | Accepts | Default |
|-----------|---------|---------|
| href | URL | — |
| background-color | CSS color | #414141 |
| color | CSS color | #ffffff |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| font-size | px | 13px |
| font-weight | string | normal |
| border-radius | string | 3px |
| border | CSS border | none |
| inner-padding | px / % | 10px 25px |
| padding | px / % | 10px 25px |
| align | left/center/right | center |
| target | string | _blank |
| width | px / % | — |
| height | px / % | — |
| text-decoration | string | none |
| text-transform | string | none |
| letter-spacing | px / em | — |
**Critical**: `href` is required — without it, button text may fail to render in some clients.
```xml
<mj-button href="https://example.com/shop" background-color="#E63946"
color="#ffffff" font-family="Arial, sans-serif" border-radius="4px"
inner-padding="12px 30px">
Shop Now
</mj-button>
```
---
## mj-divider
A horizontal rule / separator line.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| border-color | CSS color | #000000 |
| border-style | dashed/dotted/solid | solid |
| border-width | px | 4px |
| width | px / % | 100% |
| align | left/center/right | center |
| padding | px / % | 10px 25px |
```xml
<mj-divider border-color="#e0e0e0" border-width="1px" border-style="solid" />
```
---
## mj-spacer
Blank vertical space. Prefer over padding for predictable cross-client spacing.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| height | px / % | 0px |
| padding | px / % | — |
```xml
<mj-spacer height="24px" />
```
---
## mj-table
Renders a data table using plain HTML inside. Only accepts standard HTML (`<tr>`, `<td>`, `<th>`).
| Attribute | Accepts | Default |
|-----------|---------|---------|
| align | left/right/center | left |
| border | CSS border | none |
| cellpadding | integer | 0 |
| cellspacing | integer | 0 |
| color | CSS color | #000000 |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| font-size | px | 13px |
| line-height | px / % | 22px |
| table-layout | auto/fixed/initial | auto |
| width | px / % / auto | 100% |
| padding | px / % | 10px 25px |
| role | none / presentation | — |
**Use case**: Order receipts, line items, pricing tables. Always inline styles on `<td>` and `<th>` tags.
```xml
<mj-table>
<tr style="border-bottom:1px solid #ecedee;text-align:left;">
<th style="padding:0 15px 0 0;">Item</th>
<th style="padding:0 15px;">Qty</th>
<th style="padding:0 0 0 15px;">Price</th>
</tr>
<tr>
<td style="padding:8px 15px 0 0;">Widget Pro</td>
<td style="padding:8px 15px 0;">2</td>
<td style="padding:8px 0 0 15px;">$49.00</td>
</tr>
</mj-table>
```
components/head.md
# MJML Head Components Reference
## mj-attributes
Sets default attributes for components globally or by component type. Use to avoid repetitive inline styling.
```xml
<mj-head>
<mj-attributes>
<mj-all font-family="Arial, sans-serif" />
<mj-text padding="0" />
<mj-class name="hero-text" color="#ffffff" font-size="24px" />
</mj-attributes>
</mj-head>
```
- `<mj-all>` — applies attribute to every component
- `<mj-text padding="0" />` — overrides defaults for that component type
- `<mj-class name="foo" ... />` — named class, applied via `mj-class="foo"` on any component
Priority: inline attribute > mj-class > component default > mj-all
---
## mj-title
Sets the `<title>` tag and the `aria-label` on the body wrapper div (accessibility).
```xml
<mj-title>Your Email Subject or Title</mj-title>
```
---
## mj-preview
Sets the inbox preview text (visible in email client list before opening).
```xml
<mj-preview>Get 30% off this weekend only — shop now!</mj-preview>
```
No attributes. Content is plain text only.
---
## mj-font
Imports an external web font. Wraps import in MSO conditional comments so Outlook ignores it (preventing Times New Roman fallback). Always pair with a fallback font stack.
| Attribute | Accepts | Description |
|-----------|---------|-------------|
| name | string | Font name used in font-family |
| href | string | URL to hosted CSS file with @font-face |
```xml
<mj-font name="Raleway" href="https://fonts.googleapis.com/css?family=Raleway" />
<!-- Usage: font-family="Raleway, Arial, sans-serif" -->
```
**Outlook note**: Always include fallback fonts (Arial, sans-serif minimum). mj-font hides the @font-face from Outlook to prevent Times New Roman fallback.
---
## mj-breakpoint
Controls the width at which the layout switches from desktop to mobile view.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| width | px | 480px |
MJML's own default breakpoint is **480px** — a template with no `<mj-breakpoint>` emits `@media only screen and (max-width:479px)`. This is unrelated to the 600px `mj-body` width.
```xml
<mj-breakpoint width="480px" />
```
---
## mj-style
Adds custom CSS. Use `inline="inline"` to force Juice inlining (required for Gmail compatibility).
| Attribute | Accepts | Description |
|-----------|---------|-------------|
| inline | "inline" | Inlines CSS via Juice during compilation |
```xml
<!-- Inlined — safe for Gmail -->
<mj-style inline="inline">
.dark-logo { display: none; }
</mj-style>
<!-- In <head> only — use sparingly -->
<mj-style>
@media (prefers-color-scheme: dark) {
.light-logo { display: none !important; }
.dark-logo { display: block !important; }
}
</mj-style>
```
**Gmail warning**: Gmail strips `<head>` CSS unless inlined. Critical layout styles must use component attributes, not CSS classes.
---
## mj-html-attributes
Adds custom HTML attributes (data-*, ARIA) to generated HTML elements via CSS selectors. Needed for editable templates or custom ARIA roles.
```xml
<mj-html-attributes>
<mj-selector path=".custom div">
<mj-html-attribute name="data-id">42</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
```
Note: `mj-text` compiles to `<td class="..."><div>`. Target `.custom div` to reach the inner div.
components/interactive.md
# MJML Interactive Components Reference
> ⚠️ **`mj-accordion` and `mj-carousel` are documented here for reference but should not be used in production templates** (SKILL.md Rule 8). Both depend on CSS that the major clients strip: Outlook and older Gmail render an accordion **fully expanded**, defeating its purpose, and a carousel degrades to **the first image only**. The "falls back to…" notes below describe that degradation — read them as a warning, not reassurance. Prefer plain stacked sections over an accordion, and a single hero image over a carousel.
## mj-accordion
Collapsible content sections. Falls back to expanded view in clients without CSS support (Outlook, older Gmail).
Structure: `mj-accordion` > `mj-accordion-element` > `mj-accordion-title` + `mj-accordion-text`
### mj-accordion attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| border | CSS border | 2px solid black |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| icon-align | top/middle/bottom | middle |
| icon-position | left/right | right |
| icon-width | px / % | 32px |
| icon-height | px / % | 32px |
| icon-wrapped-url | URL | default + icon |
| icon-unwrapped-url | URL | default - icon |
| icon-wrapped-alt | string | + |
| icon-unwrapped-alt | string | - |
| padding | px / % | 10px 25px |
### mj-accordion-title attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| background-color | CSS color | — |
| color | CSS color | — |
| font-family | string | — |
| font-size | px | 13px |
| font-weight | string | — |
| padding | px / % | 16px |
### mj-accordion-text attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| background-color | CSS color | — |
| color | CSS color | — |
| font-family | string | — |
| font-size | px | 13px |
| font-weight | string | — |
| line-height | px / % | 1 |
| letter-spacing | px / em | — |
| padding | px / % | 16px |
```xml
<mj-accordion>
<mj-accordion-element>
<mj-accordion-title>Shipping Policy</mj-accordion-title>
<mj-accordion-text>Free shipping on orders over $50.</mj-accordion-text>
</mj-accordion-element>
<mj-accordion-element>
<mj-accordion-title>Return Policy</mj-accordion-title>
<mj-accordion-text>30-day returns, no questions asked.</mj-accordion-text>
</mj-accordion-element>
</mj-accordion>
```
---
## mj-carousel
Image gallery/slider inside an email. Falls back to first image only in unsupported clients.
Structure: `mj-carousel` > `mj-carousel-image`
### mj-carousel attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| align | left/center/right | center |
| border-radius | px / % | 6px |
| thumbnails | visible/hidden/supported | hidden |
| tb-width | px / % | — |
| tb-border | CSS border | 2px solid transparent |
| tb-border-radius | px / % | 6px |
| tb-hover-border-color | CSS color | #fead0d |
| tb-selected-border-color | CSS color | #ccc |
| icon-width | px / % | 44px |
| left-icon | URL | default arrow |
| right-icon | URL | default arrow |
| padding | px / % | — |
### mj-carousel-image attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| src | URL | — |
| alt | string | '' |
| href | URL | — |
| target | string | _blank |
| title | string | — |
| border-radius | px / % | — |
| thumbnails-src | URL | (same as src) |
| tb-border | CSS border | — |
| tb-border-radius | px / % | — |
```xml
<mj-carousel thumbnails="visible">
<mj-carousel-image src="https://cdn.example.com/product-1.jpg" alt="Product 1" href="https://example.com/p1" />
<mj-carousel-image src="https://cdn.example.com/product-2.jpg" alt="Product 2" href="https://example.com/p2" />
<mj-carousel-image src="https://cdn.example.com/product-3.jpg" alt="Product 3" href="https://example.com/p3" />
</mj-carousel>
```
---
## mj-social
Social media icon bar. Contains `mj-social-element` children.
### mj-social attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| mode | horizontal/vertical | horizontal |
| align | left/center/right | center |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| font-size | px | 13px |
| icon-size | px / % | 20px |
| icon-height | px / % | icon-size |
| icon-padding | px / % | — |
| border | CSS border (MJML 5) | — |
| border-radius | px / % | 3px |
| color | CSS color | #333333 |
| line-height | px / % | 22px |
| text-decoration | string | none |
| padding | px / % | 10px 25px |
| inner-padding | px / % | null |
**`border` (MJML 5)**: Set once on `mj-social` to cascade to every `mj-social-element`'s `<img>` (useful for accessibility tools like Litmus/Email on Acid that flag icons without a border). An element-level `border` overrides the cascade for just that icon.
### mj-social-element attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| name | string (see list below) | — |
| href | URL | — |
| src | URL | network default |
| background-color | CSS color | network default |
| alt | string | '' |
| title | string | — |
| icon-size | px / % | — |
| icon-position | left/right | left (MJML 5; had no default in v4) |
| font-size | px | 13px |
| color | CSS color | #000 |
| border | CSS border (MJML 5) | 0 |
| border-radius | px | 3px |
| padding | px / % | 4px |
| text-padding | px / % | 4px 4px 4px 0 |
| target | string | _blank |
| vertical-align | top/middle/bottom | middle |
**Supported network names** (with share URL): `facebook`, `twitter`, `x`, `google`, `pinterest`, `linkedin`, `tumblr`, `xing`
**Without share URL**: `github`, `instagram`, `web`, `snapchat`, `youtube`, `vimeo`, `medium`, `soundcloud`, `dribbble`
**To link profile instead of share**: append `-noshare` — e.g. `name="twitter-noshare"`
```xml
<mj-social font-size="13px" icon-size="24px" mode="horizontal">
<mj-social-element name="facebook-noshare" href="https://facebook.com/yourpage">
Facebook
</mj-social-element>
<mj-social-element name="instagram-noshare" href="https://instagram.com/yourhandle">
Instagram
</mj-social-element>
<mj-social-element name="linkedin-noshare" href="https://linkedin.com/company/yours">
LinkedIn
</mj-social-element>
</mj-social>
```
---
## mj-navbar
Responsive navigation bar with optional hamburger menu on mobile. Hamburger support limited to CSS-capable clients.
**Injects an external font request.** `mj-navbar` unconditionally adds a Google Fonts `<link>` and `@import` for **Ubuntu** to the head, regardless of your `mj-font` / `mj-all` settings — an unrequested third-party request in every navbar email. `<mj-all font-family="...">` does not reach the hamburger label either; set `font-family` on each `mj-navbar-link` explicitly.
**Not idempotent.** Each compile emits a fresh random 16-hex checkbox ID, so two builds of the same source differ. Same for `mj-accordion` and `mj-carousel` — do not diff their output to detect real changes.
### mj-navbar attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| align | left/center/right | center |
| base-url | string | null |
| hamburger | "hamburger" | null |
| ico-color | CSS color | #000000 |
| ico-font-size | px / % | 30px |
| ico-align | left/center/right | center |
| ico-open | string | ☰ |
| ico-close | string | ⊗ |
| ico-padding | px / % | 10px |
| padding | px / % | — |
### mj-navbar-link attributes
| Attribute | Accepts | Default |
|-----------|---------|---------|
| href | URL | — |
| color | CSS color | #000000 |
| font-family | string | Ubuntu, Helvetica, Arial, sans-serif |
| font-size | px | 13px |
| font-weight | string | — |
| padding | px / % | 15px 10px |
| text-decoration | string | none |
| text-transform | string | uppercase |
| target | string | — |
```xml
<mj-navbar base-url="https://example.com" hamburger="hamburger" ico-color="#333333">
<mj-navbar-link href="/products" color="#333333">Products</mj-navbar-link>
<mj-navbar-link href="/about" color="#333333">About</mj-navbar-link>
<mj-navbar-link href="/contact" color="#333333">Contact</mj-navbar-link>
</mj-navbar>
```
components/layout.md
# MJML Layout Components Reference
## Document Hierarchy
```
mjml
├── mj-head
└── mj-body
├── mj-wrapper (optional grouping)
│ └── mj-section
├── mj-section (row)
│ ├── mj-column (or mj-group)
│ │ └── [content components]
│ └── mj-group
│ └── mj-column
└── mj-hero (special banner section)
```
**Hard rules:**
- `mj-section` and `mj-column` cannot be nested inside another `mj-column`
- Every content component inside a column inherits 100% of the column width
- Default email width: 600px (set on `mj-body`)
---
## Root `<mjml>` tag
| Attribute | Accepts | Default | Notes |
|-----------|---------|---------|-------|
| lang | string | "und" | Sets `lang` attribute on `<html>` and body `<div>`. Use for accessibility + RTL support. e.g. `lang="en"` |
| dir | ltr / rtl | auto | Text direction |
| owa | "desktop" | none | Forces desktop view for old self-hosted Outlook.com (rare) |
```xml
<mjml lang="en" dir="ltr">
```
---
## mj-body
| Attribute | Accepts | Default |
|-----------|---------|---------|
| background-color | CSS color | — |
| width | px | 600px |
| css-class | string | — |
---
## mj-section
A horizontal row. Contains `mj-column` or `mj-group` children.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| background-color | CSS color | — |
| background-url | URL string | — |
| background-size | CSS string | auto |
| background-repeat | repeat / no-repeat | — |
| background-position | CSS keyword/% | top center |
| full-width | "full-width" | — |
| border | CSS border | — |
| border-radius | string | — |
| direction | ltr / rtl | ltr |
| gutter | px / % | — |
| padding | px / % | 20px 0 |
| text-align | left/center/right | center |
**`gutter` (MJML 5)**: Sets equal spacing between every column in the section — an alternative to adding matching left/right padding to each `mj-column` by hand. Works with `mj-group` children too, and respects `direction="rtl"`.
**Outlook background images**: Only `mj-section` and `mj-hero` support background images (MJML generates VML). Always set `background-size` and `background-color` fallback when using `background-url`. Use keyword positions (top, center) not pixels for Outlook.
```xml
<mj-section background-url="https://..." background-color="#2a3448"
background-size="cover" background-repeat="no-repeat">
```
---
## mj-column
A responsive column inside a section. Columns stack vertically on mobile by default.
Width is auto-calculated: 2 columns = 50% each, 3 columns = 33.33% each. Override with `width`.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| width | px / % | (100 / col count)% |
| background-color | CSS color | — |
| inner-background-color | CSS color | — |
| border | CSS border | — |
| border-radius | px / % | — |
| vertical-align | top/middle/bottom | top |
| padding | px / % | — |
| direction | ltr / rtl | ltr |
**vertical-align bug**: If any column in a section uses `vertical-align="middle"`, ALL columns in that section must also explicitly set `vertical-align="middle"` for it to render consistently.
```xml
<mj-section>
<mj-column width="40%">...</mj-column>
<mj-column width="60%">...</mj-column>
</mj-section>
```
---
## mj-group
Prevents columns from stacking on mobile. Wrap columns in `mj-group` to keep them side-by-side on small screens (e.g. social bars, logo headers).
| Attribute | Accepts | Default |
|-----------|---------|---------|
| width | px / % | (100 / count)% |
| background-color | CSS color | — |
| direction | ltr / rtl | ltr |
| vertical-align | CSS value | — |
**iOS stacking bug**: Even with `mj-group`, whitespace between tags can cause stacking. What prevents it is the `font-size:0px` MJML puts on the parent `<td>`. Note that in MJML 5 `--config.minify` no longer removes inter-tag whitespace (it collapses it to one space); add `--config.minifyOptions '{"collapseWhitespace":"all"}'` for the MJML 4 behavior.
```xml
<mj-section>
<mj-group>
<mj-column><mj-image src="icon1.png" alt="Facebook" /></mj-column>
<mj-column><mj-image src="icon2.png" alt="Twitter" /></mj-column>
</mj-group>
</mj-section>
```
---
## mj-wrapper
Groups multiple `mj-section` elements with a shared border, background, or padding. Useful for card-style layouts.
| Attribute | Accepts | Default |
|-----------|---------|---------|
| background-color | CSS color | — |
| background-url | URL | — |
| background-size | CSS string | auto |
| border | CSS border | — |
| border-radius | string | — |
| full-width | "full-width" | — |
| padding | px / % | 20px 0 |
| gap | px | — |
| gutter | px / % | — (MJML 5) |
```xml
<mj-wrapper border="1px solid #e0e0e0" padding="20px 0">
<mj-section>...</mj-section>
<mj-section>...</mj-section>
</mj-wrapper>
```
LICENSE
MIT License
Copyright (c) 2026 Framix
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
mjml-reference.md
# MJML General Reference
Non-component knowledge from the MJML documentation — architecture, behaviors, gotchas, and validation.
---
## mj-body Automatic ARIA Injection
MJML automatically wraps email body content in a `<div>` with the following accessibility attributes:
```html
<div role="article" aria-roledescription="email" aria-label="[value from mj-title]" lang="[from mjml tag]" dir="[from mjml tag]">
```
This means:
- Always set `<mj-title>` — it populates `aria-label` on the wrapper div, used by screen readers to identify the email
- Always set `lang` on the root `<mjml>` tag — it flows through to this div
---
## Ending Tags
The following components are **ending tags** — their content is raw HTML/text, NOT processed by the MJML engine. You cannot nest other MJML components inside them.
- `mj-text`
- `mj-button`
- `mj-table`
- `mj-raw`
- `mj-accordion-title`
- `mj-accordion-text`
- `mj-navbar-link`
- `mj-social-element`
**Implications:**
- You can use any HTML inside these (`<strong>`, `<a>`, `<span>`, etc.)
- Characters `<` and `>` used as literal text should be escaped as `<` and `>`
- **MJML 5 relaxed this.** A `<` followed by a space is now fine: `Price < 100 dollars` compiles and survives intact under `minify`. In MJML 4 the same input was a hard `Parse Error`. The `htmlnano` swap fixed it
- **But a *tag-like* `<` still breaks — silently.** `{{#if a<b}}yes{{/if}}` (a `<` immediately followed by a letter) compiles at exit `0` and emits mangled output: `{{#if a<b}}yes{{ if}}< div> </b}}yes{{>`. No error, no warning. Never treat exit `0` as proof here
- The fix is `<!-- htmlmin:ignore -->` around the tag, which preserves it byte-exact (markers must come in pairs and are stripped from the output). **Do not** substitute `<` for a template tag: the entity survives literally into the sent HTML (`{{#if a<b}}`), so the ESP never receives a `<` and the condition never evaluates
- `mj-html-attributes` and inline `mj-style` require re-parsing — can conflict with ending tag content
---
## Validation Levels
MJML validates component nesting and attributes. Three levels:
| Level | Behavior |
|-------|----------|
| `soft` (default) | Logs warnings, renders anyway |
| `strict` | Throws error and stops rendering on any syntax mistake |
| `skip` | No validation, renders as-is |
**CLI usage:**
```bash
# Validate without rendering
npx mjml --validate template.mjml
# Set validation level
npx mjml template.mjml --config.validationLevel=strict
npx mjml template.mjml --config.validationLevel=skip
```
**Recommended**: Use `strict` during development to catch nesting errors early.
---
## Hybrid Fluid Design — How Widths Work
MJML uses a "hybrid" or "spongy" approach:
- **Modern clients** (Apple Mail, Gmail app): fluid percentage widths
- **Outlook**: fixed-width ghost tables (VML-based)
Column widths are auto-calculated from the section width (default 600px):
- 1 column = 100% (600px)
- 2 columns = 50% each (300px each)
- 3 columns = 33.33% each (200px each)
- Set explicit `width` on `mj-column` to override (must sum to 100% or 600px)
Columns stack vertically on mobile. Use `mj-group` to prevent stacking.
---
## What MJML Actually Generates
MJML compiles semantic tags into complex nested tables. Example — `mj-button` becomes:
```html
<table cellpadding="0" cellspacing="0" style="border:none;border-radius:3px;" align="center">
<tbody><tr>
<td style="background-color:#414141;border-radius:3px;color:#ffffff;cursor:auto;" align="center" valign="middle" bgcolor="#414141">
<a class="mj-content" href="#"
style="display:inline-block;text-decoration:none;background-color:#414141;border:1px solid #414141;border-radius:3px;color:#ffffff;font-size:13px;font-weight:bold;padding:15px 30px;"
target="_blank">
Hello There!
</a>
</td>
</tr></tbody>
</table>
```
This is why hand-coding email HTML is error-prone — MJML handles this complexity automatically.
---
## css-class and container-background-color
Available on **all body components**:
- **`css-class`**: Adds a class to the root HTML element generated by that component. Used to target with `mj-style` or `mj-html-attributes`. Note: the class lands on an outer element (usually `<td>`), not necessarily the visible content. Check compiled HTML to know which element gets the class.
- **`container-background-color`**: Sets the background color of the cell wrapping a content component inside a column. Different from the column's `background-color` — affects the per-component cell. **Exception (MJML 5): `mj-hero` no longer accepts it** — it was valid in v4 and is now `Attribute container-background-color is illegal`, a hard exit-1 failure under `--config.validationLevel=strict`.
```xml
<!-- container-background-color creates a highlight behind just this text block -->
<mj-text container-background-color="#fff9e6" padding="16px">
Important notice
</mj-text>
```
---
## mj-include File Types
| type attribute | Effect |
|---------------|--------|
| (omitted) | Treats file as `.mjml` — parsed and merged |
| `type="css"` | Inserted as `mj-style` |
| `type="css" css-inline="inline"` | Inserted as `mj-style inline="inline"` (Juice-inlined) |
| `type="html"` | Inserted as `mj-raw` — no parsing |
Base path override: `--config.filePath ./partials/` — allows includes to resolve from a fixed directory regardless of where the compiled file lives.
**MJML 5**: includes are disabled unless explicitly enabled (`--config.allowIncludes true` CLI / `ignoreIncludes: false` Node API), and resolve only from the source file's directory plus any `--config.includePath` allowlist. See `components/advanced.md` for full detail.
---
## Preprocessors (.mjmlconfig.js)
JavaScript functions that receive raw MJML XML as a string **before parsing**. Useful for integrating templating engines (Handlebars, Liquid) at preview/dev time.
```js
// .mjmlconfig.js
module.exports = {
preprocessors: [
(xml) => xml.replace(/\{\{firstName\}\}/g, 'Preview Name')
]
}
```
Production: leave template tags in place (e.g. `{{firstName}}`), let your ESP (SendGrid, AWS SES) inject values. Use `<mj-raw>` to protect template syntax from the MJML parser.
---
## Node.js API (if not using the CLI)
This skill compiles via `npx mjml` (see `compilation.md`), but if the user wants to call MJML from a Node script (e.g. inside an ESP-sending pipeline), note that **`mjml2html` is asynchronous in MJML 5** — it returns a `Promise`, not a synchronous result as in MJML 4.
```js
import mjml2html from 'mjml'
const { html, errors } = await mjml2html(mjmlSource, {
validationLevel: 'strict',
minify: true,
ignoreIncludes: false, // only if the template uses mj-include
includePath: ['./partials'],
})
```
Key options mirror the CLI flags: `validationLevel`, `minify`, `minifyOptions`, `filePath`, `includePath`, `ignoreIncludes`, `allowMixedSyntax`, `sanitizeStyles`, `templateSyntax`, `beautify`.
---
## MJML 5 Output Differences
Two v5 changes affect the compiled HTML itself, independent of the template you write:
- **Minifier swap** — `--config.minify=true` now runs `htmlnano` + `cssnano` instead of `html-minifier`. CSS is minified more (total output is smaller than v4), but **HTML whitespace is minified less**: `htmlnano`'s `collapseWhitespace` collapses inter-tag runs to a single space where `html-minifier` removed them entirely. Any snapshot test or raw-HTML diff against v4 output will report changes. Pass `--config.minifyOptions '{"collapseWhitespace":"all"}'` to restore v4 behavior — the JSON-string form is required, `--config.minifyOptions.collapseWhitespace=all` fails to parse. (`cssnano` uses the `lite` preset by default.)
- **`<body>` is generated by `mj-body`** rather than a global skeleton. Notably, `css-class` on `mj-body` now lands on the `<body>` tag itself — in v4 it landed on the child `<div>`. External CSS or ESP tooling that targeted that class by position may no longer match.
Both are output-shape changes only — no template edits are required.
---
## Gmail 102KB Clip
Gmail clips emails exceeding **102KB**. The entire bottom of the email is hidden with a "View entire message" link.
**Prevention**: Always compile with minification:
```bash
npx mjml template.mjml -o output.html --config.minify=true
```
In MJML 4 minification also stripped whitespace between `inline-block` columns. **It no longer does** — see MJML 5 Output Differences above. Stacking is prevented by the `font-size:0px` MJML emits on the parent `<td>`; add `--config.minifyOptions '{"collapseWhitespace":"all"}'` if you want the v4 stripping back.
SKILL.md
---
name: email-html-mjml
description: Responsive HTML email template generation using MJML 5.x framework. Use when the user asks to create, generate, design, or build an email template — including welcome emails, promotional blasts, transactional templates, newsletters, or any responsive email. Also use when the user asks to compile MJML to HTML, work with or edit existing MJML templates or `.mjml` files, or troubleshoot email rendering issues across clients.
license: MIT
compatibility: Requires Node.js 20 or later (LTS 20/22/24) for npx mjml compilation. Install MJML per project (npm install -D mjml). Works with Claude Code and Claude.ai.
metadata:
author: Framix
version: 2.0.0
documentation: https://github.com/framix-team/skill-email-html-mjml
---
# email-html-mjml — Responsive Email Developer
Generate valid, cross-client MJML 5.x templates and compile them to production-ready HTML. The primary goal is compatibility: Outlook (2013–365), Gmail (web/app), Apple Mail, and major mobile clients. Every output must be compilable with `--config.validationLevel=strict` and survive Gmail's 102KB clip limit.
---
## Workflow
1. **Gather requirements** — Infer email type, brand colors, and content from the user's message and conversation context. Ask only for what is genuinely missing and blocking progress (e.g., no colors provided and the layout has branded sections). Never front-load a questionnaire.
2. **Plan layout** — Decide and announce the structure before writing code (single-column, 2-col grid, hero + content, etc.)
3. **Load component references** — Read the relevant file(s) from the Component Index below before writing any MJML
4. **Generate MJML** — Write complete, valid MJML starting from `<mjml>` with a full `<mj-head>`
5. **Compile** — Follow `compilation.md`. Run `npx mjml` with `--config.minify=true`
6. **Deliver both files** — Always output `.mjml` source AND compiled `.html`
---
## 9 Engineering Rules
1. **Structural Integrity** — All visual content MUST be in `<mj-column>` inside `<mj-section>`. Sections cannot be nested.
2. **Responsive Defaults** — Assume 600px width. Use `<mj-group>` to prevent mobile stacking for side-by-side elements (social bars, logo rows). Use `<mj-section gutter="...">` for equal spacing between columns instead of padding tricks.
3. **Outlook Compatibility** — Use `<mj-font>` for web fonts (prevents Times New Roman fallback). Always provide a fallback stack (Arial, sans-serif). For `<mj-section>` background images, always set both `background-size` and a fallback `background-color`.
4. **Gmail Optimization** — Use `inline="inline"` on `<mj-style>` for custom CSS. Prefer component attributes (`color`, `font-size`) over CSS classes for critical styles.
5. **Dark Mode** — Include dark mode support when explicitly requested or when the email has a light background that would cause harsh forced-inversion. See the Dark Mode Pattern below.
6. **Accessibility** — Every `<mj-image>` MUST have `alt`. Always set `<mj-title>` (populates `aria-label`). Maintain WCAG 2.1 AA 4.5:1 contrast. For heading roles, use `mj-html-attributes` — direct `role`/`aria-level` attributes on `mj-text` are illegal under strict validation (see Accessibility Checklist below).
7. **Styling Efficiency** — Use `<mj-attributes>` with `<mj-all>`, component defaults, and `<mj-class>` to eliminate repetitive inline styles.
8. **Hero Sections** — Use `<mj-hero>` for full-bleed hero banners; it falls back to a regular section in unsupported clients. Avoid `<mj-accordion>` and `<mj-carousel>` — client support is too poor to be useful.
9. **Templating Support** — Plain interpolations (`{{firstName}}`) need no wrapping — they pass through text, attributes, `mj-title` and `mj-preview` untouched. Only block tags containing `<` or `>` (`{% if x < 5 %}`, `{{#if a<b}}`) need `<!-- htmlmin:ignore -->` protection, or minify silently corrupts them. An `<mj-raw>` between content components inside a column emits its content *between* `</tr>` and `<tr>` — invalid markup; put `mj-raw` between sections, and keep templating tags out of `<mj-style>` (see `compilation.md`).
---
## Critical Gotchas
**MJML 5 `mj-include` is disabled by default:**
- `<mj-include>` tags are silently ignored unless includes are explicitly enabled — a deliberate security change from MJML 4
- CLI: add `--config.allowIncludes true`. Node API: pass `ignoreIncludes: false`
- Scope allowed directories with `--config.includePath` (CLI) or `includePath` (Node) — only MJML, HTML, and CSS includes are supported
- A partial outside the template's own directory is denied unless its directory is allowlisted
- **`--config.validationLevel=strict` does not catch this** — a dropped include still exits `0` and still writes the `.html`. A clean compile is NOT proof the include landed; grep the output for known partial content before delivering
- If a compiled template is missing content you expected from an include, check for this first
**Outlook:**
- Background images: VML only generated for `<mj-section>` and `<mj-hero>` — nowhere else
- Background positioning: keyword values only (`top`, `center`, `bottom`) — pixel values ignored
- Always pair `background-repeat="no-repeat"` with explicit `background-size`
- Font fallback: `<mj-font>` hides `@font-face` from Outlook via MSO conditional comments
**Gmail:**
- Use component attributes for critical layout — CSS classes may be stripped
- 102KB clip: always compile with `--config.minify=true`
**iOS / Android stacking:**
- **MJML 5's minify no longer strips whitespace between tags** — `htmlnano` collapses runs to a single space where `html-minifier` removed them (verified: v4 emits `<![endif]--><div class="mj-column-per-50"`, v5 emits `<![endif]--> <div ...`). The anti-stacking defence that still works is the `font-size:0px` MJML puts on the parent `<td>`, which is emitted in both versions
- To restore v4 whitespace stripping: `--config.minifyOptions '{"collapseWhitespace":"all"}'` (JSON string form required)
- Whitespace between tags causes stacking even inside `<mj-group>`
**Vertical-align bug:**
- If any column in a section sets `vertical-align`, ALL columns in that section must explicitly set it
**JavaScript:**
- JS is completely blocked in all email clients (Gmail, Outlook, Apple Mail, iOS Mail). No `onclick`, no clipboard API, no interactivity of any kind. Interactive-looking elements (copy buttons, toggles) are purely decorative.
---
## Dark Mode Pattern
```xml
<mj-head>
<mj-raw>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
</mj-raw>
<!-- Light logo visible by default; dark logo hidden -->
<mj-style inline="inline">
.dark-logo { display: none !important; }
</mj-style>
<!-- Dark mode overrides -->
<mj-style>
@media (prefers-color-scheme: dark) {
.light-logo { display: none !important; }
.dark-logo { display: block !important; }
}
</mj-style>
</mj-head>
```
Safe neutrals: `#121212` (not `#000000`) and `#F1F1F1` (not `#FFFFFF`) — prevents jarring forced inversions.
---
## Accessibility Checklist
- `<mj-title>` is set (screen reader email label + `aria-label`)
- `lang` attribute on root `<mjml>` tag
- `alt` on every `<mj-image>` and `<mj-social-element>`
- Heading role set via `mj-html-attributes` (NOT as a direct attribute on `mj-text`):
```xml
<!-- In mj-head -->
<mj-html-attributes>
<mj-selector path=".email-heading div">
<mj-html-attribute name="role">heading</mj-html-attribute>
<mj-html-attribute name="aria-level">1</mj-html-attribute>
</mj-selector>
</mj-html-attributes>
<!-- On the component -->
<mj-text css-class="email-heading" ...>Heading text</mj-text>
```
- 4.5:1 contrast ratio on all text/background pairs
- No text baked into images — always use live `<mj-text>` blocks
---
## Component Index
Before writing any MJML, read the component file(s) for the components you'll use.
| Group | Components | Load when | File |
|-------|-----------|-----------|------|
| Head | mj-attributes, mj-font, mj-style, mj-preview, mj-breakpoint, mj-html-attributes | Setting up head, global styles | `components/head.md` |
| Layout | mj-body, mj-section, mj-column, mj-group, mj-wrapper | Building structure / grid | `components/layout.md` |
| Content | mj-text, mj-image, mj-button, mj-divider, mj-spacer, mj-table | Adding content blocks | `components/content.md` |
| Interactive | mj-accordion, mj-carousel, mj-social, mj-navbar | Interactive or social elements | `components/interactive.md` |
| Advanced | mj-hero, mj-raw, mj-include | Hero banners, template tags, partials | `components/advanced.md` |
General reference (hierarchy, ending tags, validation, width math, Gmail clip): `mjml-reference.md`
---
## Compilation
Read `compilation.md` for the full workflow. Key command:
```bash
npx mjml template.mjml -o dist/template.html --config.minify=true --config.validationLevel=strict
```
Hard rules:
- Never `npm install -g mjml`
- Always use `npx` or `./node_modules/.bin/mjml`
- If mjml not in `package.json`, suggest `npm install -D mjml`
---
## Examples
All four compile clean under `--config.validationLevel=strict --config.minify=true` on MJML 5.4.0.
| File | Use as a reference for |
|------|------------------------|
| `assets/examples/basic-layout.mjml` | Structure only. MJML docs example: 6-section layout (header, image hero + button, intro, 2-col image+text, 3-col icons, social row). Intentionally bare-bones — no `mj-head`, no dark mode, placeholder copy. Not a production template. |
| `assets/examples/order-confirmation.mjml` | Transactional. Full `mj-head` (`mj-attributes` + `mj-class` + `mj-font` + `mj-html-attributes`), receipt `mj-table` with `scope="col"` headers, Handlebars in `mj-raw` with `htmlmin:ignore`, conditional block, and `mj-include` of `partials/footer.mjml`. **Requires `--config.allowIncludes true`.** |
| `assets/examples/promo-sale.mjml` | Promotional. `mj-hero` and `mj-section` background images (VML generated), `mj-navbar`, `mj-group` non-stacking row, `mj-social`, `mj-breakpoint`, plus the MJML 5 `gutter` / `border` / `inner-padding` attributes. |
| `assets/examples/newsletter.mjml` | Editorial + **dark mode**. The full dark-mode pattern from above, `mj-wrapper`, `mj-html-attributes` for `aria-level` 1 and 2, and `vertical-align` set on every column in each section. |
All copy, image URLs, and brands in these files are placeholders — replace them; do not ship them as-is.
---
## Output
Always deliver:
1. **`<name>.mjml`** — complete MJML source (editable, version-controllable)
2. **`<name>.html`** — compiled output (production-ready, send via ESP)
Name files after the email type: `welcome.mjml`, `promo-sale.mjml`, `order-confirmation.mjml`