
This is a solid MJML starter for anyone who needs to ship responsive HTML emails that actually render correctly in Outlook, Gmail, and mobile clients. It enforces strict validation, includes dark mode patterns, and walks you through the full workflow from gathering requirements to compiling minified HTML. The nine engineering rules are opinionated but practical, especially around Outlook's VML quirks and Gmail's 102KB clip limit. Framix built this with accessibility baked in (proper heading roles via mj-html-attributes, contrast requirements) and includes gotcha warnings for vertical-align bugs and whitespace stacking. You get both the editable MJML source and production-ready HTML every time. Good for transactional templates, newsletters, or any email work where cross-client compatibility isn't optional.
npx -y skills add framix-team/skill-email-html-mjml --skill email-html-mjml --agent claude-codeInstalls into .claude/skills of the current project.
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.
<mjml> with a full <mj-head>compilation.md. Run npx mjml with --config.minify=true.mjml source AND compiled .html<mj-column> inside <mj-section>. Sections cannot be nested.<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.<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.inline="inline" on <mj-style> for custom CSS. Prefer component attributes (color, font-size) over CSS classes for critical styles.<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).<mj-attributes> with <mj-all>, component defaults, and <mj-class> to eliminate repetitive inline styles.<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.{{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).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--config.allowIncludes true. Node API: pass ignoreIncludes: false--config.includePath (CLI) or includePath (Node) — only MJML, HTML, and CSS includes are supported--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 deliveringOutlook:
<mj-section> and <mj-hero> — nowhere elsetop, center, bottom) — pixel values ignoredbackground-repeat="no-repeat" with explicit background-size<mj-font> hides @font-face from Outlook via MSO conditional commentsGmail:
--config.minify=trueiOS / Android stacking:
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--config.minifyOptions '{"collapseWhitespace":"all"}' (JSON string form required)<mj-group>Vertical-align bug:
vertical-align, ALL columns in that section must explicitly set itJavaScript:
onclick, no clipboard API, no interactivity of any kind. Interactive-looking elements (copy buttons, toggles) are purely decorative.<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.
<mj-title> is set (screen reader email label + aria-label)lang attribute on root <mjml> tagalt on every <mj-image> and <mj-social-element>mj-html-attributes (NOT as a direct attribute on mj-text):
<!-- 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>
<mj-text> blocksBefore 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
Read compilation.md for the full workflow. Key command:
npx mjml template.mjml -o dist/template.html --config.minify=true --config.validationLevel=strict
Hard rules:
npm install -g mjmlnpx or ./node_modules/.bin/mjmlpackage.json, suggest npm install -D mjmlAll 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.
Always deliver:
<name>.mjml — complete MJML source (editable, version-controllable)<name>.html — compiled output (production-ready, send via ESP)Name files after the email type: welcome.mjml, promo-sale.mjml, order-confirmation.mjml