Guide
How to QA an MJML email before you send it
MJML solves rendering. These seven habits solve the content problems MJML can't see.
What MJML handles — and what it doesn't
MJML compiles simple components (<mj-section>, <mj-column>, <mj-button>) into the nested tables, Outlook conditional comments and responsive CSS that email clients need. That's why compiled MJML rarely trips compatibility checks.
But MJML only knows about structure. It can't tell that a link is a placeholder, an image needs alt text, a UTM value is empty or a TODO is still in the copy. Those mistakes compile straight through — so check the output, not just the source.
# compile
npx mjml email.mjml -o email.html
# log validation errors (exits with code 1 if there are any — useful in CI)
npx mjml -v email.mjml
# or refuse to compile invalid MJML
npx mjml -l strict email.mjml -o email.html1. Set the language on <mjml>
Unless you set it, recent MJML versions output lang="und" (“undetermined”), so screen readers have to guess the language. Set it once on the root tag:
<mjml lang="en" dir="ltr">2. Add a title and a preheader
<mj-title> sets the document title (shown in browser views and announced by some screen readers). <mj-preview> sets the preheader — the preview text inbox lists show next to your subject. Without it, clients show whatever text comes first, often “View in browser” or alt text.
<mj-head>
<mj-title>Join us for the autumn meetup</mj-title>
<mj-preview>Talks, demos and drinks with the team on 24 October. Save your seat.</mj-preview>
</mj-head>3. Give every mj-image an alt
If you leave out alt, the compiled image ends up with no useful alt text. On a linked image that's a link with no name for screen-reader users, and an empty box when images are blocked. Describe what the image says or where it leads; use alt="" only for purely decorative images.
<mj-image src="https://cdn.acme.com/email/autumn-meetup.jpg"
href="https://acme.com/events/autumn-meetup"
alt="Autumn community meetup, 24 October" />4. Give every mj-button a real href
Templates often start with href="#", and it's easy to miss when the button looks finished. A button without a real destination does nothing for the people most ready to click. Check every <mj-button> and linked <mj-image> before you compile for the last time.
<mj-button href="https://acme.com/events/autumn-meetup/rsvp?utm_source=newsletter&utm_medium=email&utm_campaign=autumn-meetup">
Save my seat
</mj-button>5. Keep tracking consistent
Long MJML files make it easy to tag one link and forget the next. Use the same utm_source, utm_medium and utm_campaign on every link, never leave a value empty, and don't hard-code UTMs if your email platform already adds them. The link and UTM guide covers this in detail.
6. Watch the size: Gmail clips at about 102 KB
MJML output is verbose, and Gmail hides everything after roughly 102 KB of HTML behind a “View entire message” link — often including your footer, unsubscribe link and open tracking. To stay well under it:
- Set shared styles once with
<mj-attributes>and<mj-class>instead of repeating attributes on every component. - Minify the compiled HTML in your build step, making sure the minifier keeps Outlook conditional comments.
- Don't embed images as base64 — host them.
- Leave headroom: your email platform adds tracking code after you upload.
<mj-attributes>
<mj-all font-family="Helvetica, Arial, sans-serif" />
<mj-text font-size="16px" line-height="24px" color="#18181b" />
<mj-button background-color="#4f46e5" border-radius="6px" font-weight="bold" />
</mj-attributes>7. Be careful with mj-raw
Anything inside <mj-raw> is passed through untouched. That's useful for custom snippets, but MJML can't protect you there: forms, scripts, position or flexbox inside <mj-raw> will break in the same clients they always do. Paste the compiled output into the checker and it will catch those too.
A simple MJML QA workflow
- Build in MJML with
lang,<mj-title>,<mj-preview>and shared<mj-attributes>. - Validate (
mjml -v) and compile. - Paste the compiled HTML into the checker and fix what it finds in your .mjml source.
- Send a test to yourself, click every link and read it with images off.