Developer Tools

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.

Olivier Bazoud
November 5, 2026
11 min read

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.

At a glance
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

Figures read on November 5, 2026.

Good fit
  • 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
Not a fit
  • 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.

JavaScript
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

Pros
  • 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
Cons
  • Reference engine needs Node.js
  • Three major lines in ten months
  • mj-include ignored 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-style takes an inline="inline" attribute, and the CLI and Node API expose juicePreserveTags. MJML 5 replaced html-minifier with htmlnano and cssnano for minify. Beautify moved to Prettier, then Biome, and has been js-beautify again 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-section wraps its tables in <!--[if mso | IE]> and uses VML for background images in Outlook.

  • Validation: three levels, strict, soft (the default) and skip, plus mjml -v on 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.

ProjectLanguageWhat it isStatusRepo
MRMLRustReimplementation 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)PythonPure 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)PythonPython wrapper around a Rust extension built on MRML.Active. v1.4.2 on 2026-09-01, 113 stars.mgd020/mjml-python
django-mjmlPython{% 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-railsRubyActionMailer 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)ElixirRust NIF bindings to MRML. Downloads a precompiled NIF by default.Active. 6.0.0 on 2026-05-16, 144 stars.adoptoposs/mjml_nif
gomjmlGoNative 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
mgmlGoNative 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)GoThe 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
mjml4jJavaPure Java 17 port, targets MJML 4.18.0.Active. 1.2.2 on 2026-06-09, 45 stars.digitalfondue/mjml4j
mjml-javaJavaPure 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:

TypeScript
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

Claimed by the project
  • Most popular email clients
  • Outlook
  • Per-component compatibility matrix
Not handled
  • 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-column gets a 100% inline width, and the desktop widths come only from a min-width: 480px media 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-style are dropped. Only <mj-style inline="inline"> goes through juice, and MJML does not reinsert its @media blocks. Put responsive and dark-mode rules in a separate, non-inline mj-style, with !important so they beat the inline styles that components write.
  • Upgrading v4 code breaks quietly in two places. mjml2html has been async since v5, so mjml2html(src).html returns undefined: write const { html } = await mjml2html(src). mj-include is also ignored in the Node API without a warning unless you pass ignoreIncludes: false (the CLI warns), and neither change is in the upgrade guide.
  • Outlook for Windows gets VML only for backgrounds. mj-section, mj-wrapper and mj-hero emit VML for background images, but mj-button has none, so rounded corners disappear in Outlook, and the button is fully clickable only with a width in px. Do not set background-url on both a wrapper and its child section.
  • The documented validation error does not exist. validationLevel: 'strict' throws a plain Error with an errors array, not the MJMLValidationError with getErrors() that the docs describe. The default soft level renders invalid markup and only lists the problems in the returned errors, 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 latest for production. The v6 alpha ships under the npm dist-tag experimental, 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-include is 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.