I still remember the first responsive email I coded by hand. Nested tables six deep, a media query Outlook ignored, and a Friday evening lost to a column that stacked on every phone except the one the client owned. Then a colleague sent me a link to MJML, and the same layout took twenty lines.

That was a long time ago now. MJML came out of Mailjet in 2016, went open source under MIT, and has been quietly maintained ever since; the current release is 5.4. It’s had ten years to either become the standard or get replaced by something better, and the interesting thing is that neither happened. It’s still the best tool for one specific job and still the wrong tool for a couple of others. This is a fair account of which is which.

What MJML actually does

MJML is a markup language that compiles to email HTML. You write components like mj-section, mj-column and mj-text, and the compiler turns them into the table-based, inline-styled, Outlook-conditional HTML that email clients require. You never touch that output. You paste it into your ESP or send it through your own pipeline.

Here’s the whole idea in one example:

<mjml>
  <mj-body>
    <mj-section>
      <mj-column>
        <mj-text>Hello,<br />This is a sample email created with MJML.</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Compiled, that becomes:

<table border="0" cellpadding="0" cellspacing="0" width="100%">
  <tbody>
    <tr>
      <td>
        <table border="0" cellpadding="0" cellspacing="0" width="100%">
          <tbody>
            <tr>
              <td align="left">
                Hello,<br>This is a sample email created with MJML.
              </td>
            </tr>
          </tbody>
        </table>
      </td>
    </tr>
  </tbody>
</table>

Trimmed for reading, but you get the shape. Twelve lines in, forty out, and every one of the forty is a line you didn’t have to know how to write.

Where MJML is still the best tool

Responsive by default. Put two mj-column elements in a section and they sit side by side on desktop and stack on mobile, with no media query written by you. This is the feature people come for and it hasn’t stopped being the reason to stay.

The client compatibility is somebody else’s job. Outlook’s rendering engine, Gmail’s CSS support, Apple Mail’s quirks: the compiler knows about them and you don’t have to. When a client changes behaviour, the fix lands in the next MJML release rather than in your template.

A component set that covers most emails. mj-hero for a full-width banner, mj-button, mj-image, mj-social for the icon row, mj-divider, mj-spacer. mj-carousel and mj-accordion do interactive things in the clients that support them and degrade sensibly in the ones that don’t.

It lives in a repo. MJML files are text, so they diff, branch and review like code. mj-include pulls in shared partials, so the header and footer exist once. A team that keeps its templates in git and compiles them in CI gets consistency that no drag-and-drop editor can match.

The ecosystem is real. There’s the CLI on npm, an official VS Code extension with live preview, a browser playground, mjml-react for teams who’d rather write JSX, and GrapesJS has an MJML mode for people who want a visual editor that outputs MJML underneath.

Where MJML gets in the way

The output is big. All that safety comes as markup. A busy template can compile to a size where Gmail clips the message and hides the footer behind a “view entire message” link, unsubscribe included. Watch the compiled size, not the source size.

You can’t reach the pixels. The abstraction is the point, until you need a layout the components don’t express. mj-raw lets you drop in hand-written HTML, but the moment you use it much you’re maintaining two systems. I’ve seen teams reach that point and quietly go back to hand-coded templates, and I’ve seen others decide the layout wasn’t worth it. Both were right.

It knows nothing about your ESP. MJML compiles to HTML and stops. Merge tags, conditional blocks, dynamic sections, the language of your ESP has to be written into the MJML source as plain text and survive compilation intact. It usually does, but you find out by testing.

No logic of its own. Loops, conditions and data come from whatever wraps it: a templating layer, mjml-react, or your ESP after the fact. MJML is the last step, not the whole pipeline.

Marketers mostly won’t use it. If the person building the email lives in Klaviyo’s or Mailchimp’s editor, MJML is a tool for the developer who builds their templates, not for them. That’s fine, but it decides who this is for.

MJML versus the alternatives

The honest split is by who’s doing the work.

If a developer builds templates and marketers fill them in, MJML plus a templating layer, compiled in CI, is hard to beat. If marketers build every email in the ESP’s editor, the ESP’s editor wins and MJML is a solution to a problem you don’t have. And if you need a layout MJML can’t express, hand-coded HTML with a framework like Cerberus or Foundation for Emails is the right call, with the cost being that client compatibility becomes your job again.

There isn’t much middle ground, and searching for “MJML alternatives” mostly turns up tools that have made a different choice on that same split.

Putting dynamic content inside an MJML template

One thing MJML handles better than people expect. MJML compiles at build time, which sounds like it rules out anything that changes after send. It doesn’t, because open-time content in email is an image URL, and an image URL survives compilation untouched.

So a countdown timer, a dynamic image with the reader’s name in it, or a product block that reflects live stock is just an mj-image pointing at a URL that renders when the email is opened:

<mj-image
  href="https://next.alterable.com/countdown-timers/click/YOUR-TIMER-ID"
  src="https://next.alterable.com/countdown-timers/YOUR-TIMER-ID.png"
  alt="Sale ends soon"
  width="510px" />

Compile it and you get a correctly wrapped, responsive image tag. Open the email on Thursday and the image is drawn on Thursday. MJML did the layout, the URL did the rest, and neither had to know about the other. Merge tags in the URL go in as text, the same way they would in body copy.

A workflow that holds up

For what it’s worth, this is the setup I’ve seen work best in teams that stuck with MJML:

  1. Templates in git, one file per email, shared header and footer through mj-include.
  2. A templating layer above MJML for logic and data, so MJML only ever sees final markup.
  3. Compile in CI with the CLI, and fail the build if the output crosses your size limit.
  4. Paste or upload the compiled HTML to the ESP, and test on real clients before the first send, because the compiler is good but it isn’t a rendering test.
  5. Anything that has to be true at open time, a timer, a live image, a product, goes in as an image URL, so the template never needs recompiling for it.

MJML doesn’t make email design easy. It makes the part that was never worth your time go away, and ten years in that’s still worth having.