yeti logo icon
Close Icon
contact us
Yeti postage stamp
We'll reply within 24 hours.
Thank you! Your message has been received!
A yeti hand giving a thumb's up
Oops! Something went wrong while submitting the form.

Using MJML to Create Responsive HTML Emails

By
Will Harris
-
June 14, 2021

Email is everywhere. Often when building an application there will be some functionality that requires sending emails to your users.

The unfortunate truth is that crafting emails in HTML can still be a tedious and time-consuming task. There are a lot of email clients out there, and they all parse HTML a *little* bit differently. Some don't even support HTML, which can cause email delivery issues.

And making your emails responsive for mobile devices? You'll need pretty deep understanding of the differences between all of the different email clients to land on a one-size-fits-all email design.

Luckily we have options when it comes to creating responsive HTML emails.

The first, and least ideal, is to build out our emails from scratch. This is a viable option if your emails designs are very simple and you are alright with some degree of degradation on older email clients.

Another option is to find a boilerplate HTML template somewhere online that can be modified to fit your needs. A popular one is Email Boilerplate on GitHub.

The third and, in our opinion, most robust option is to use an email framework. Choose a framework when you need to produce many email templates with shared components, and when you will have multiple developers working with your email templates. In these situations having a robust, standardized way of creating emails can be very beneficial. Two of the most popular frameworks that we considered were MJML and Foundation for Emails.

In this post we'll tale a look at MJML, a tool we've found useful for creating responsive HTML email designs. We will implement an email design together step-by-step to demonstrate how MJML can simplify your email development process.

What is MJML?

MJML, or Mailjet Markup Language, is an open source project created by the development team at Mailjet. Their team has spent years diving deep into the complexities of HTML email and understanding the differences between all the different email clients. The result is a markup language that abstracts away an entire layer of complexity related to responsive HTML email design.

But why do we need this layer of abstraction? Why can't we just design HTML emails using the same HTML and CSS used throughout the web?

Unlike the web, where a few browsers handle most of the traffic, emails are read in a huge number of email clients. The issue is that each of these clients support different subsets of HTML and CSS — often this difference is quite large.

For example, the desktop versions of Microsoft's Outlook email client use Microsoft Word to render emails. That's right, a word processor used to parse HTML emails. This results in pretty abysmal CSS support for emails viewed in Outlook, alongside many frustrating platform-specific quirks:

* No support for background images in divs or table cells
* No support for CSS float of position properties
* No support for text shadow
* Poor support for margin and padding
* Poor support for CSS width and height
* Issues with background colors for nested elements

Another example is Google's email client, Gmail. Gmail supports only a subset of CSS properties, and will only parse styles in the document's head. However, unlike normal CSS parsers in the browser, if Gmail encounters an error if throws away all of the styles in the head. This can lead to some quite frustrating debugging trying to determine which CSS rule is causing you to lose all of your styles.

As a result the most effective way to write HTML emails to ensure that they work across different email clients is to use table-based layouts and minimal CSS.

MJML smoothes over all of these differences with an easy-to-use syntax. Let's take a look at how we can use MJML to create beautiful, responsive emails by implementing the following design.

How Do We Use MJML?

To get started, we'll first create a new JavaScript project. If you don't already have Node.js and npm installed, you can find installation instructions on the Node website.

To get the assets that I'm using for this demonstration, or if you'd rather not follow along on your own, you can clone this public GitHub repository.

First we'll create an empty directory and run npm init -y to initialize the project. We'll then add mjml  to our project's dependencies with npm install mjml.

Next we'll create a file called `index.mjml` at the root of our project. This file will hold all of the MJML markup for our email. The .mjml extension allows this file to be parsed into HTML by the mjml package.

Inside of this file we'll start by laying out our sections. Looking at our design, we have four distinct rows of content. This means that we will need four mj-section tags, one for each row of our email.

<mjml>
 <mj-body>
   <mj-section></mj-section>

     <mj-section></mj-section>

     <mj-section></mj-section>

     <mj-section></mj-section>
 </mj-body>
<mjml>

Sections are then broken up into columns. The default behavior is to divide the section's space into as many columns as we declare.

Looking back at our design, our first section has just one column, so it will occupy the entire width of the first section. This is the default behavior of mj-column, so we don't need to define any width attributes in this situation.

<mjml>
<mj-body>
<mj-section>
<mj-column>

</mj-column>
</mj-section>

<mj-section></mj-section>

<mj-section></mj-section>

<mj-section></mj-section>
</mj-body>
<mjml>

We'll then add the top section's background, text, and button. mj-text elements can contain any HTML elements with any attributes, and are the perfect choice for displaying text in our email.

<mjml>   <mj-body>
    <mj-section background-url="./assets/img/yeti-bg.png" background-size="cover" full-width="full-width">
<mj-column>
  <mj-text align="center" font-size="14px" color="#45474e" padding-bottom="10px" padding-top="45px">
    <span style="font-size: 30px; line-height: 30px;">
             Discover the adventure of inventing
         </span>
         <br />
         <br />
         With <span style="color: #00707E">Yeti</span>
 </mj-text>
 <mj-button align="center" background-color="#00707E" color="#fff" border-radius="24px" href="https://yeti.co" font-family="Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="45px" padding-top="10px" text-transform="uppercase">
         Subscribe
       </mj-button>
      </mj-column>
     </mj-section>

     <mj-section></mj-section>

     <mj-section></mj-section>
 </mj-body>
<mjml>

And that about does it for our top section.

Notice that so far we've put all of our styles in-line with our MJML markup. We also have the option of storing our styles in mj-head tags above the mj-body element. This would allow us to either send our styles along as part of the document's head, or to have MJML inline the styles for us. I chose to inline the styles here to keep everything co-located and easy to demonstrate, but it is by no means a requirement when authoring MJML.

Also notice that we're using padding for all of the spacing in our email design. This is because support for marginin email clients is super spotty, while most support padding quite well. This is a consistent theme throughout MJML and HTML email in general: always prefer padding.

Next, let's tackle the second section. This section is a bit more involved because we'll need to implement three equal-width columns, each with an image and some text. First we'll add the columns to our second section.

<mj-section>
 <mj-column></mj-column>

 <mj-column></mj-column>

 <mj-column><mj-column>
<mj-section>

The default behavior for mj-column is to evenly divide the space of the containing mj-section element (which is `600px` by default). Because we want our columns to be equal width, we do not need to define a width on the columns at all. MJML deals with dividing the space evenly for us.

The layout for each column is going to be essentially the same, just with different content. Let's throw all that markup into our columns.

<mj-section background-color="#ffffff" full-width="full-width">
 <mj-column>
   <mj-image src="./assets/img/crown.png" alt="crown icon" width="50px" />
   <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px">
     <span style="font-size: 14px; color: #00707E">Best Yetis</span>
     <br />
     <br />
     Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
     eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
  </mj-text>
    </mj-column>

    <mj-column>
     <mj-image src="./assets/img/coins.png" alt="stack of coins icon" width="50px" />
     <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px">
      <span style="font-size: 14px; color: #00707E">Maximum Wow</span>
      <br />
      <br />
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
      eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
   </mj-text>
 </mj-column>

<mj-column>
  <mj-image src="./assets/img/headset.png" alt="person wearing a headset icon" width="50px" />
  <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px" padding-top="3px">
     <span style="font-size: 14px; color: #00707E">People Talking</span>
     <br />
     <br />
     Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
     eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
  </mj-text>
</mj-column>

<mj-section>

Here we used the mj-image component to render a png icon in each column, setting the width of each to 50px using the widthattribute. We've also added more text content and line breaks with the mj-text element.

Next we'll implement the third section of our email, which contains a full-width background color, more text, and a dividing element in between.

<mjml><mj-section background-color="#00707E" full-width="full-width">

 <mj-column>
   <mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px">
     Why choose us?
   </mj-text>
   <mj-divider border-color="#fff" border-style="solid" border-width="1px" width="60%" padding-bottom="20px" padding-top="20px" />
   <mj-text align="center" color="#f8d5d1" font-size="11px" padding-bottom="25px">
     Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
     eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
     minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum
     dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
     incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
     quis nostrud exercitation ullamco laboris.
  </mj-text>
 </mj-column>

</mj-section>

We set the background color of this section by passing a color value to background-color attribute of the mj-section element. We also set full-width="full-width" on the section element because otherwise, as mentioned above, the section will default to 600px wide.

We've also included a mj-divider element here, which we're using to separate our two mj-text elements visually. This is similar to the horizontal rule element in HTML (which is poorly supported by email clients) and is customized like an HTML border, hence the border- attributes.

And finally we get to the bottom section of our email. This section contains an image and some text side-by-side, with a call to action for the user to "Read More."

<mj-section background-color="#ffffff" full-width="full-width">

 <mj-column>
   <mj-image src="./assets/img/meeting.png" alt="Image of people working on computers." padding-bottom="20px" padding-top="20px" />
 </mj-column>

 <mj-column>
   <mj-text align="left" color="#9da3a3" font-size="11px" padding-bottom="25px" padding-top="25px">
     <span style="font-weight: bold; font-size: 14px; color: #45474e">
       Great newsletter for the most Yeti company out there
     </span>
     <br />
     <br />
     Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
     eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
     minim veniam.
   </mj-text>
   <mj-button align="left" background-color="#00707E" color="#fff" border-radius="24px" font-size="11px" href="https://yeti.co" font-family="Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="45px" padding-top="10px" text-transform="uppercase">
     Read more
   </mj-button>
 </mj-column>

</mj-section>

Again, we do not need to set explicit widths for the two columns in this section because we want them to be equal width. We can modify this behavior by adding width properties to each mj-column element.

That's all the MJML markup that we'll need for this email. The entire document should look like this:

<mjml>
 <mj-body background-color="#d7dde5">
   <mj-section background-url="./assets/img/yeti-bg.png" background-size="cover" full-width="full-width" background-repeat="no-repeat">
     <mj-column>
       <mj-text align="center" font-size="14px" color="#45474e" padding-bottom="10px" padding-top="45px">
         <span style="font-size: 30px; line-height: 30px;">
           Discover the adventure of inventing
         </span>
         <br />
         <br />
         Only with <span style="color: #00707E">Yeti</span>
       </mj-text>
      <mj-button align="center" background-color="#00707E" color="#fff" border-radius="24px" href="https://yeti.co" font-family="Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="45px" padding-top="10px" text-transform="uppercase">
         Subscribe
       </mj-button>
     </mj-column>
   </mj-section>

   <mj-section background-color="#ffffff" full-width="full-width">
     <mj-column>
       <mj-image src="./assets/img/crown.png" alt="crown icon" width="50px" />
       <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px">
         <span style="font-size: 14px; color: #00707E">Best Yetis</span>
         <br />
        <br />
         Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
         eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
       </mj-text>
    </mj-column>
    <mj-column>
       <mj-image src="./assets/img/coins.png" alt="stack of coins icon" width="50px"></mj-image>
       <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px">
         <span style="font-size: 14px; color: #00707E">Maximum Wow</span>
         <br />
        <br />
         Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
         eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
       </mj-text>
     </mj-column>
    <mj-column>
       <mj-image src="./assets/img/headset.png" alt="person wearing a headset icon" width="50px"></mj-image>
       <mj-text align="center" color="#9da3a3" font-size="11px" padding-bottom="30px" padding-top="3px">
         <span style="font-size: 14px; color: #00707E">People Talking</span>
         <br />
         <br />
         Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque
         eleifend sagittis nunc, et fermentum est ullamcorper dignissim.
       </mj-text>
     </mj-column>
   </mj-section>

   <mj-section background-color="#00707E" full-width="full-width">
     <mj-column>
       <mj-text align="center" color="#ffffff" font-size="18px" padding-bottom="10px">
         Why choose us?
       </mj-text>
       <mj-divider border-color="#fff" border-style="solid" border-width="1px" width="60%" padding-bottom="20px" padding-top="20px" />
       <mj-text align="center" color="#f8d5d1" font-size="11px" padding-bottom="25px">
         Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
         eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
         minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum
         dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
         incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
         quis nostrud exercitation ullamco laboris.
       </mj-text>
     </mj-column>
   </mj-section>

   <mj-section background-color="#ffffff" full-width="full-width">
     <mj-column>
     <mj-image src="./assets/img/meeting.png" alt="Image of people working on computers." padding-bottom="20px" padding-top="20px" />
     </mj-column>
     <mj-column>
       <mj-text align="left" color="#9da3a3" font-size="11px" padding-bottom="25px" padding-top="25px">
         <span style="font-weight: bold; font-size: 14px; color: #45474e">
           Great newsletter for the most Yeti company out there
         </span>
         <br />
         <br />
         Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
         eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
         minim veniam.
      </mj-text>
       <mj-button align="left" background-color="#00707E" color="#fff" border-radius="24px" font-size="11px" href="https://yeti.co" font-family="Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-bottom="45px" padding-top="10px" text-transform="uppercase">
         Read more
       </mj-button>
     </mj-column>
   </mj-section>
 </mj-body>
</mjml>

How Do We Generate the MJML Email?

Now that we've written all of our markup, how do we actually generate the final MJML output? We'll add a couple scripts to our project's package.json file (which was created for us when we ran npm init -y).

"scripts": {
   "build": "mjml index.mjml --output output.html",
   "watch": "mjml --watch index.mjml --output output.html"
}

These scripts are very similar — they both tell MJML to parse the index.mjml file and output the result to a file named output.html. The build command runs this process once, while the watch command runs a persistent Node process that watches index.mjml and recompiles on every file change. This makes the watch command super useful for development.

If we inspect the contents of output.html we can see that MJML has converted the markup into a responsive HTML table layout for us. I'm not going to include the final HTML in this post as its nearly 400 lines long, but it is definitely interesting to dig through to see all of the work that MJML is doing for us.

Conclusion

In summary, in this post we implemented a static email design using MJML to create a responsive, cross-client compatible HTML email. This email contains all of the knowledge and strategies the Mailjet team have accumulated over the years.

When compared with trying to write these nested table layouts by hand, the MJML approach is a huge timesaver. And you can be confident that the emails your company is shipping will create a consistent experience for most of your users.

We at Yeti have gotten a ton of value out of MJML, and we think you will too.

Further Reading / Resources

Will is a software developer at Yeti. In his free time you can find him riding bikes, hiking in the hills and searching  Bay Area restaurants for delicious vegetarian food.

You Might also like...

colorful swirlsAn Introduction to Neural Networks

Join James McNamara in this insightful talk as he navigates the intricate world of neural networks, deep learning, and artificial intelligence. From the evolution of architectures like CNNs and RNNs to groundbreaking techniques like word embeddings and transformers, discover the transformative impact of AI in image recognition, natural language processing, and even coding assistance.

A keyboardThe Symbolicon: My Journey to an Ineffective 10-key Keyboard

Join developer Jonny in exploring the Symbolicon, a unique 10-key custom keyboard inspired by the Braille alphabet. Delve into the conceptualization, ideas, and the hands-on process of building this unique keyboard!

Cross-Domain Product Analytics with PostHog

Insightful product analytics can provide a treasure trove of valuable user information. Unfortunately, there are also a large number of roadblocks to obtaining accurate user data. In this article we explore PostHog and how it can help you improve your cross-domain user analytics.

Browse all Blog Articles

Ready for your new product adventure?

Let's Get Started