Skip to main content
Technology & EngineeringEmail Template304 lines

Mjml

Building responsive email templates with the MJML markup language and toolchain

Quick Summary34 lines
You are an expert in MJML for building responsive, cross-client email templates.

## Key Points

- `<mj-section>` — a full-width row (maps to a table row).
- `<mj-column>` — a column within a section. Columns stack vertically on mobile by default.
- Columns auto-distribute width equally, or you can set explicit `width` as a percentage or pixel value.
- Use `<mj-attributes>` in the head to define global defaults for font family, sizes, and colors. This keeps templates consistent and avoids repetition.
- Set `<mj-preview>` text to control what recipients see in inbox previews. Keep it under 90 characters.
- Use `<mj-include>` for shared headers, footers, and style blocks across templates. This makes updates to branding a single-file change.
- Always specify `alt` text on `<mj-image>` for accessibility and for clients that block images by default.
- Set `validationLevel: "strict"` when rendering programmatically to catch malformed markup at build time rather than discovering broken layout in production.
- Keep the overall email body width at 600px (the MJML default) for maximum compatibility.
- Use `<mj-wrapper>` when multiple sections need to share a single background color or image.
- Combine MJML with a templating engine (Handlebars, Nunjucks, Liquid) for dynamic content injection rather than trying to embed logic in MJML itself.
- **Nesting columns inside columns**: MJML does not support nested columns. Use separate `<mj-section>` blocks to achieve more complex layouts.

## Quick Example

```
emails/
  partials/
    header.mjml
    footer.mjml
  welcome.mjml
```

```xml
<mj-feature-card icon-url="https://example.com/icon.png" title="Fast Delivery">
  Get your order in 2 business days.
</mj-feature-card>
```
skilldb get email-template-skills/MjmlFull skill: 304 lines
Paste into your CLAUDE.md or agent config

MJML — Email Templates

You are an expert in MJML for building responsive, cross-client email templates.

Core Philosophy

Overview

MJML (Mailjet Markup Language) is an open-source markup language that abstracts away the complexity of responsive email HTML. You write semantic MJML tags and the compiler produces optimized, table-based HTML with inline CSS that renders consistently across email clients. It eliminates the need to hand-code nested tables and client-specific hacks.

Core Concepts

Installation and CLI

npm install mjml

# Compile a single file
npx mjml input.mjml -o output.html

# Watch mode for development
npx mjml --watch input.mjml -o output.html

Basic Document Structure

Every MJML document follows this skeleton:

<mjml>
  <mj-head>
    <mj-title>Email Subject Fallback</mj-title>
    <mj-preview>Preview text shown in inbox</mj-preview>
    <mj-attributes>
      <mj-all font-family="Helvetica, Arial, sans-serif" />
      <mj-text font-size="16px" line-height="24px" color="#333333" />
      <mj-button background-color="#5469d4" color="#ffffff" border-radius="4px" />
    </mj-attributes>
    <mj-style>
      .highlight { color: #5469d4; }
    </mj-style>
  </mj-head>
  <mj-body background-color="#f6f9fc">
    <mj-section background-color="#ffffff" padding="20px">
      <mj-column>
        <mj-text>Hello World</mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Layout Model

MJML uses a section/column grid system:

  • <mj-section> — a full-width row (maps to a table row).
  • <mj-column> — a column within a section. Columns stack vertically on mobile by default.
  • Columns auto-distribute width equally, or you can set explicit width as a percentage or pixel value.
<!-- Two-column layout, 60/40 split -->
<mj-section>
  <mj-column width="60%">
    <mj-text>Main content</mj-text>
  </mj-column>
  <mj-column width="40%">
    <mj-image src="https://example.com/photo.jpg" alt="Photo" />
  </mj-column>
</mj-section>

Core Components

ComponentPurpose
<mj-text>Block of text content; supports inline HTML
<mj-image>Responsive image with src, alt, width
<mj-button>Call-to-action button with href
<mj-divider>Horizontal rule
<mj-spacer>Vertical spacing
<mj-social>Social media icon row
<mj-navbar>Navigation bar with links
<mj-table>Raw HTML table for data grids
<mj-raw>Escape hatch for raw HTML
<mj-hero>Hero section with background image
<mj-wrapper>Groups sections under a shared background

Implementation Patterns

Branded Header and Footer

<mjml>
  <mj-head>
    <mj-attributes>
      <mj-all font-family="Helvetica, Arial, sans-serif" />
    </mj-attributes>
  </mj-head>
  <mj-body background-color="#f4f4f4">
    <!-- Header -->
    <mj-section background-color="#1a1a2e" padding="20px 0">
      <mj-column>
        <mj-image
          src="https://example.com/logo-white.png"
          alt="Company Logo"
          width="150px"
          align="center"
        />
      </mj-column>
    </mj-section>

    <!-- Body -->
    <mj-section background-color="#ffffff" padding="40px 24px">
      <mj-column>
        <mj-text font-size="22px" font-weight="bold">
          Welcome aboard!
        </mj-text>
        <mj-text>
          We're thrilled to have you. Here's what you can do next.
        </mj-text>
        <mj-button href="https://example.com/dashboard">
          Go to Dashboard
        </mj-button>
      </mj-column>
    </mj-section>

    <!-- Footer -->
    <mj-section padding="20px 24px">
      <mj-column>
        <mj-social font-size="12px" icon-size="24px" mode="horizontal">
          <mj-social-element name="twitter" href="https://twitter.com/example" />
          <mj-social-element name="linkedin" href="https://linkedin.com/company/example" />
        </mj-social>
        <mj-text align="center" font-size="11px" color="#999999">
          © 2026 Company Inc. | <a href="{{unsubscribe_url}}" style="color:#999;">Unsubscribe</a>
        </mj-text>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>

Hero Section with Background Image

<mj-hero
  mode="fluid-height"
  background-height="500px"
  background-width="600px"
  background-url="https://example.com/hero-bg.jpg"
  background-color="#1a1a2e"
  padding="100px 24px"
>
  <mj-text
    align="center"
    font-size="28px"
    color="#ffffff"
    font-weight="bold"
    padding-bottom="16px"
  >
    Season Sale — 30% Off
  </mj-text>
  <mj-button href="https://example.com/sale" background-color="#ff6b6b">
    Shop Now
  </mj-button>
</mj-hero>

Reusable Partials with mj-include

Split large templates into partials:

emails/
  partials/
    header.mjml
    footer.mjml
  welcome.mjml
<!-- welcome.mjml -->
<mjml>
  <mj-head>
    <mj-include path="./partials/head.mjml" />
  </mj-head>
  <mj-body>
    <mj-include path="./partials/header.mjml" />

    <mj-section background-color="#ffffff" padding="40px 24px">
      <mj-column>
        <mj-text>Welcome content here.</mj-text>
      </mj-column>
    </mj-section>

    <mj-include path="./partials/footer.mjml" />
  </mj-body>
</mjml>

Programmatic Rendering with Node.js

const mjml = require("mjml");
const fs = require("fs");
const Handlebars = require("handlebars");

// Load MJML template with Handlebars placeholders
const source = fs.readFileSync("./emails/welcome.mjml", "utf8");

// Compile MJML to HTML first
const { html, errors } = mjml(source, {
  validationLevel: "strict",
  filePath: "./emails/",  // required for mj-include resolution
});

if (errors.length) {
  console.error("MJML errors:", errors);
  process.exit(1);
}

// Then apply Handlebars for dynamic data
const template = Handlebars.compile(html);
const finalHtml = template({
  username: "Alice",
  activationUrl: "https://example.com/activate?token=abc",
});

Custom Components

const { registerComponent, BodyComponent } = require("mjml-core");

class MjFeatureCard extends BodyComponent {
  static endingTag = true;
  static allowedAttributes = {
    "icon-url": "string",
    title: "string",
  };

  render() {
    return this.renderMJML(`
      <mj-section padding="16px">
        <mj-column width="80px">
          <mj-image src="${this.getAttribute("icon-url")}" width="48px" />
        </mj-column>
        <mj-column>
          <mj-text font-weight="bold">${this.getAttribute("title")}</mj-text>
          <mj-text>${this.getContent()}</mj-text>
        </mj-column>
      </mj-section>
    `);
  }
}

registerComponent(MjFeatureCard);

Usage:

<mj-feature-card icon-url="https://example.com/icon.png" title="Fast Delivery">
  Get your order in 2 business days.
</mj-feature-card>

Best Practices

  • Use <mj-attributes> in the head to define global defaults for font family, sizes, and colors. This keeps templates consistent and avoids repetition.
  • Set <mj-preview> text to control what recipients see in inbox previews. Keep it under 90 characters.
  • Use <mj-include> for shared headers, footers, and style blocks across templates. This makes updates to branding a single-file change.
  • Always specify alt text on <mj-image> for accessibility and for clients that block images by default.
  • Set validationLevel: "strict" when rendering programmatically to catch malformed markup at build time rather than discovering broken layout in production.
  • Keep the overall email body width at 600px (the MJML default) for maximum compatibility.
  • Use <mj-wrapper> when multiple sections need to share a single background color or image.
  • Combine MJML with a templating engine (Handlebars, Nunjucks, Liquid) for dynamic content injection rather than trying to embed logic in MJML itself.

Common Pitfalls

  • Nesting columns inside columns: MJML does not support nested columns. Use separate <mj-section> blocks to achieve more complex layouts.
  • Using CSS classes for layout: MJML inlines most styles, but custom classes defined in <mj-style> only work for properties that email clients support. Avoid relying on classes for structural layout.
  • Forgetting filePath in programmatic rendering: Without it, <mj-include> paths cannot be resolved and the compiler silently drops the included content.
  • Assuming background images work everywhere: <mj-hero> background images are not supported in all Outlook versions. Always set a background-color fallback.
  • Overriding responsive behavior unintentionally: Adding fixed pixel widths on columns prevents them from stacking on mobile. Use percentage widths when mobile stacking is desired.
  • Putting block-level HTML inside <mj-text>: While <mj-text> accepts inner HTML, block elements like <div> can break the table structure in some clients. Stick to inline elements and <p> tags.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add email-template-skills

Get CLI access →