MJML
MJML compiles XML-style markup to table-based HTML email. See a working example, what changed in v5, how it renders, and the ports outside Node, dated.
MJML is a markup language for responsive HTML email. You write semantic tags such as <mj-section>, <mj-column> and <mj-text> in an XML-style .mjml file, and the engine returns the nested tables, inline CSS and Outlook conditional comments you would otherwise write by hand. The reference engine runs on Node.js with no account, and ports in Rust, Go, Python, Ruby, Java and Elixir cover other stacks.
- Language
- JavaScript (Node.js), XML-style markup
- License
- MIT, free
- Maintainer
- Mailjet
- Latest release
- 5.4.1 (2026-09-10)
- Weekly npm downloads
- 2,686,448
- Source
- mjmlio/mjml
Figures read on November 5, 2026.
- One template language for a whole company, not tied to a front-end framework
- A backend that is not JavaScript, through a port
- Layouts that must hold up in Outlook without hand-written tables
- Any sending provider: the engine renders locally
- A team that wants React or Vue components rather than markup
- A stack where adding Node.js is not an option and no port covers your MJML version
Minimal example
This is the example from the project README, for Node.js. mjml2html is an async function, so you await it.
import mjml2html from 'mjml'const htmlOutput = await mjml2html(`<mjml><mj-body><mj-section><mj-column><mj-text>Hello World!</mj-text></mj-column></mj-section></mj-body></mjml>`)
The same README shows a command line form: mjml input.mjml -o output.html. Add -w to recompile when the file changes.
Pros and cons
- Reference engine that the ports follow
- Outlook ghost tables and background VML generated
- Built-in validation levels and CLI
- Runs standalone, no Mailjet account
- Ports for Rust, Go, Python, Ruby, Java, Elixir
- Reference engine needs Node.js
- Three major lines in ten months
mj-includeignored by default since v5- No dark-mode feature in v5
- No stated support policy for v4
The details behind each point are in How it works, Limits and gotchas and Maintenance.
How it works
The engine does three things.
-
Tables: the project describes the output as "responsive email HTML with nested tables and inline CSS".
-
Inlining: CSS is inlined with
juice.mj-styletakes aninline="inline"attribute, and the CLI and Node API exposejuicePreserveTags. MJML 5 replacedhtml-minifierwithhtmlnanoandcssnanofor minify. Beautify moved to Prettier, then Biome, and has beenjs-beautifyagain since 5.2.0. -
Outlook conditionals: the output skeleton declares the VML and Office XML namespaces and emits
<!--[if mso]>and<o:OfficeDocumentSettings>blocks.mj-sectionwraps its tables in<!--[if mso | IE]>and uses VML for background images in Outlook. -
Validation: three levels,
strict,soft(the default) andskip, plusmjml -von the command line.
Custom components are supported through a .mjmlconfig file, and mj-include pulls in other .mjml, HTML and CSS files when you enable it.
Ports and renderers outside Node
The MJML repository keeps a ports list headed "These contributions are not directly supported by the MJML team". It has no Go entry: gomjml, mgml and mjml-go are separate projects, listed below with the rest. Each project here has its own repository and maintainer. Statuses were read on 2026-11-05. Check the MJML version a port targets before you move a template to it: the Go ports target MJML 4, and the stable Felix Schwarz Python release targets MJML 4.
| Project | Language | What it is | Status | Repo |
|---|---|---|---|---|
| MRML | Rust | Reimplementation of MJML. Library, CLI, WebAssembly and Python extension. Inlining of mj-style inline needs the optional css-inline Cargo feature. | Active. Latest release 6.0.1 on 2026-04-25, 513 stars. | jdrouet/mrml |
mjml-python (PyPI mjml) | Python | Pure Python, no Node.js or Rust. Optional css_inlining extra. | Active. Stable 0.12.0 on 2025-12-27, pre-release 1.0.0a1 for MJML 5.4 on 2026-09-12. | FelixSchwarz/mjml-python |
mjml-python (PyPI mjml-python) | Python | Python wrapper around a Rust extension built on MRML. | Active. v1.4.2 on 2026-09-01, 113 stars. | mgd020/mjml-python |
| django-mjml | Python | {% mjml %} template tag for Django. Calls the Node.js mjml, which you install separately. | Active. 1.6 on 2026-08-25, 284 stars. | liminspace/django-mjml |
| mjml-rails | Ruby | ActionMailer template handler. Uses Node.js mjml, or MRML with use_mrml = true. | Active. 5.0.0 on 2026-04-23 (adds MJML 5), 326 stars. | sighmon/mjml-rails |
| mjml (Hex) | Elixir | Rust NIF bindings to MRML. Downloads a precompiled NIF by default. | Active. 6.0.0 on 2026-05-16, 144 stars. | adoptoposs/mjml_nif |
| gomjml | Go | Native Go port, targets MJML 4.15.3. The library does not inline CSS. | Active, pre-1.0. v0.13.0 on 2026-09-09, 125 stars. | preslavrachev/gomjml |
| mgml | Go | Native Go port, pinned to MJML 4.18.0. minify and beautify are not implemented. | Active, pre-1.0. v0.1.4 on 2026-09-19, 0 stars. | krozhkov/mgml |
| mjml-go (Boostport) | Go | The JavaScript MJML compiled to WebAssembly, run with wazero. Bundles MJML 4.15.3. | Maintenance-only. Last tag 2025-08-15, 121 stars. | Boostport/mjml-go |
| mjml4j | Java | Pure Java 17 port, targets MJML 4.18.0. | Active. 1.2.2 on 2026-06-09, 45 stars. | digitalfondue/mjml4j |
| mjml-java | Java | Pure Java, no dependencies, Spring module. First release 2026-02-09. | Active. 1.0.5 on 2026-11-02, 8 stars. | jcputney/mjml-java |
Two Python projects share the name mjml-python, so pip install mjml and pip install mjml-python give different engines. The Elixir package, mjml-rails (with use_mrml), the mrml-python extension and mgd020's mjml-python all sit on MRML, so they share its gaps and its release pace.
Send it with an email API
mjml2html returns an object with an html string. Pass it to the Nuntly TypeScript SDK:
import mjml2html from 'mjml';import { Nuntly } from '@nuntly/sdk';const nuntly = new Nuntly({ apiKey: process.env.NUNTLY_API_KEY! });const { html } = await mjml2html(`<mjml><mj-body><mj-section><mj-column><mj-text>Hi Jane</mj-text></mj-column></mj-section></mj-body></mjml>`);await nuntly.emails.send({from: 'Acme <hello@example.com>',to: 'jane@example.org',subject: 'Welcome',html,});
See Send your first email for the client setup. From Go, Python, Ruby or Elixir, render with the port and POST the HTML string to https://api.nuntly.com/emails with the same from, to, subject and html fields and an Authorization: Bearer <API key> header. For Better Auth, Better Email (@nuntly/better-email) is an open-source plugin with React Email and MJML renderers.
The HTML you pass ends up as one part of a MIME message. MIME and message format cover what travels on the wire.
Email client support
- Most popular email clients
- Outlook
- Per-component compatibility matrix
- Dark mode (v5)
- VML for buttons
- Carousel and accordion in Gmail
The documentation says "MJML is responsive by design on most-popular email clients, even Outlook", and its FAQ points to a per-component matrix on the project website. We did not read the matrix cells or test the output. The project documents its own gaps: the mj-navbar hamburger menu works only in Apple Mail below the breakpoint, and mj-carousel interactivity depends on the client.
Outlook on Windows gets fixed-width tables, so the desktop layout holds without media queries, but VML covers only section, wrapper and hero backgrounds. In Gmail, caniemail lists <style> as limited to 16 KB on desktop and unsupported in mobile webmail, and :checked, which the carousel and accordion rely on, as unsupported. MJML 5 has no dark-mode feature and caniemail lists prefers-color-scheme as unsupported in Gmail and Outlook on Windows, so dark mode stays hand-written CSS. The v6 alpha adds support-dark-mode. Before shipping, send the delivered HTML to the clients you target, since a preview in a browser is not an email client. Ports make their own claims, and Felix Schwarz's README says minor differences from the JavaScript output are possible.
Limits and gotchas
- Columns are mobile-first, so a client or sending tool that strips head CSS shows the stacked layout on desktop. Every
mj-columngets a 100% inline width, and the desktop widths come only from amin-width: 480pxmedia query in the<head>. caniemail lists no<style>support in Gmail mobile webmail, SFR, La Poste and some German webmail, so when columns stack in a test, check the delivered HTML for empty<style>tags before blaming MJML. - Media queries inside an inline
mj-styleare dropped. Only<mj-style inline="inline">goes throughjuice, and MJML does not reinsert its@mediablocks. Put responsive and dark-mode rules in a separate, non-inlinemj-style, with!importantso they beat the inline styles that components write. - Upgrading v4 code breaks quietly in two places.
mjml2htmlhas beenasyncsince v5, somjml2html(src).htmlreturnsundefined: writeconst { html } = await mjml2html(src).mj-includeis also ignored in the Node API without a warning unless you passignoreIncludes: false(the CLI warns), and neither change is in the upgrade guide. - Outlook for Windows gets VML only for backgrounds.
mj-section,mj-wrapperandmj-heroemit VML for background images, butmj-buttonhas none, so rounded corners disappear in Outlook, and the button is fully clickable only with a width in px. Do not setbackground-urlon both a wrapper and its child section. - The documented validation error does not exist.
validationLevel: 'strict'throws a plainErrorwith anerrorsarray, not theMJMLValidationErrorwithgetErrors()that the docs describe. The defaultsoftlevel renders invalid markup and only lists the problems in the returnederrors, so check that array in CI.
Maintenance
Active: nine stable 5.x releases from 2026-04-16 to 2026-09-10, a 6.0.0 alpha on 2026-10-09, and a commit on master on 2026-10-23.
- Stay on
latestfor production. The v6 alpha ships under the npm dist-tagexperimental, lists five breaking changes and needs Node 22 or later. - Check Node and options before moving to v5. It dropped Node 16 and 18, and
mj-includeis now ignored unless you opt in. - No stated v4 support policy. The last v4 release is 4.18.0 (2025-12-08), and several ports still target MJML 4.
Compare
Other tools that produce email-client HTML:
- React Email vs MJML: markup against components, with the same template in both.
- React Email: React components instead of markup, maintained by Resend.
- JSX Email: React components too, with its own maintainer.
- mjml-react: React components named after MJML tags, compiled through MJML.
- Maizzle: Vue single-file components with Tailwind CSS.
- svelte-email: Svelte components.
- @unitpost/email: JSX or a string template, with Tailwind-style classes.
Back to the email frameworks hub. Hermes, a Go struct for transactional layouts, is another option outside this list.
FAQ
Do I need a Mailjet account to use MJML?
No. MJML is MIT licensed and renders on your machine with npm install mjml. Mailjet maintains it and offers a hosted editor and an MJML HTTP API, but the engine does not need either, and you can send the HTML through any provider.
Can I use MJML without Node.js?
Yes, through a port. MRML renders MJML in Rust, gomjml and mgml in Go, mjml-go in Go through WebAssembly, and there are two Python ports and an Elixir package. mjml-rails can use MRML instead of Node, while django-mjml still calls Node.js mjml. Ports can trail the Node engine, so test your templates on the port you pick.
Does MJML inline CSS?
Yes. MJML inlines CSS with juice, and mj-style accepts an inline="inline" attribute to inline a style block. Some ports differ: gomjml does not inline, and MRML inlines mj-style inline only with its optional css-inline feature.
What changed between MJML 4 and MJML 5?
MJML 5.0.0 shipped on 2026-04-16. The upgrade notes list new minifiers (htmlnano and cssnano), mj-include ignored by default, the body element generated by mj-body, looser border-radius values, the removal of mjml-migrate, and Node 16 and 18 dropped. Read these before you upgrade a v4 project.
Is MJML still maintained?
Yes, as read on 2026-11-05. The last stable release is v5.4.1 from 2026-09-10, a v6.0.0 alpha followed on 2026-10-09, and the last commit on master is from 2026-10-23.