Understanding Magento 2 themes can be hard. The structure of the files, especially the less files can get confusing. One of the hard things to understand is how to style your emails.

In this tutorial, we'll cover how we can easily style emails in Magento 2.


Prerequisites: Create a Theme

If you don't already have a theme created, we'll go over the steps quickly. If you have a theme you can skip this. Keep in mind we're not actually going to explain everything about creating a theme here, as this can be long.

First, create the directories app/design/VENDER/THEME_NAME where VENDOR is usually substituted with the company or owner's name, and THEME_NAME is the name of the them.

Next, inside THEME_NAME, create theme.xml with the following content:

<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>THEME_NAME</title>
    <parent>Magento/blank</parent>
</theme>

Replace THEME_NAME with the name of your theme.

Create registration.php with the following content:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/VENDOR/THEME_NAME', __DIR__);

Replace VENDOR and THEME_NAME with your own.

Create composer.json with the following content:

{
    "name": "VENDOR/THEME_NAME",
    "description": "N/A",
    "type": "magento2-theme",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "config": {
        "sort-packages": true
    },
    "version": "100.4.2",
    "require": {
        "php": "~7.3.0||~7.4.0",
        "magento/framework": "103.0.*"
    },
    "autoload": {
        "files": [
            "registration.php"
        ]
    }
}

Replace VENDOR and THEME_NAME with your own.

Create etc/view.xml with the following content:

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
    <media>
        <images module="Magento_Catalog">
            <image id="bundled_product_customization_page" type="thumbnail">
                <width>140</width>
                <height>140</height>
            </image>
            <image id="cart_cross_sell_products" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="cart_page_product_thumbnail" type="small_image">
                <width>110</width>
                <height>160</height>
            </image>
            <image id="category_page_grid" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="category_page_list" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="customer_account_my_tags_tag_view" type="small_image">
                <width>100</width>
                <height>100</height>
            </image>
            <image id="customer_account_product_review_page" type="image">
                <width>285</width>
                <height>285</height>
            </image>
            <image id="customer_shared_wishlist" type="small_image">
                <width>113</width>
                <height>113</height>
            </image>
            <image id="gift_messages_checkout_small_image" type="small_image">
                <width>75</width>
                <height>75</height>
            </image>
            <image id="gift_messages_checkout_thumbnail" type="thumbnail">
                <width>100</width>
                <height>100</height>
            </image>
            <image id="mini_cart_product_thumbnail" type="thumbnail">
                <width>156</width>
                <height>156</height>
            </image>
            <image id="new_products_content_widget_grid" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="new_products_content_widget_list" type="small_image">
                <width>270</width>
                <height>270</height>
            </image>
            <image id="new_products_images_only_widget" type="small_image">
                <width>78</width>
                <height>78</height>
            </image>
            <image id="product_base_image" type="image">
                <width>265</width>
                <height>265</height>
            </image>
            <image id="product_comparison_list" type="small_image">
                <width>140</width>
                <height>140</height>
            </image>
            <image id="product_page_image_large" type="image"/>
            <image id="product_page_image_medium" type="image">
                <width>700</width>
                <height>700</height>
            </image>
            <image id="product_page_image_small" type="thumbnail">
                <width>90</width>
                <height>90</height>
            </image>
            <image id="product_swatch_image_large" type="image"/>
            <image id="product_swatch_image_medium" type="image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="product_swatch_image_small" type="thumbnail">
                <width>88</width>
                <height>110</height>
            </image>
            <image id="product_page_main_image" type="image">
                <width>700</width>
                <height>700</height>
            </image>
            <image id="product_page_main_image_default" type="image">
                <width>700</width>
                <height>700</height>
            </image>
            <image id="product_page_more_views" type="thumbnail">
                <width>90</width>
                <height>90</height>
            </image>
            <image id="product_stock_alert_email_product_image" type="small_image">
                <width>76</width>
                <height>76</height>
            </image>
            <image id="product_small_image" type="small_image">
                <width>135</width>
                <height>135</height>
            </image>
            <image id="product_thumbnail_image" type="thumbnail">
                <width>75</width>
                <height>75</height>
            </image>
            <image id="recently_compared_products_grid_content_widget" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="recently_compared_products_images_names_widget" type="thumbnail">
                <width>75</width>
                <height>90</height>
            </image>
            <image id="recently_compared_products_images_only_widget" type="thumbnail">
                <width>76</width>
                <height>76</height>
            </image>
            <image id="recently_compared_products_list_content_widget" type="small_image">
                <width>270</width>
                <height>207</height>
            </image>
            <image id="recently_viewed_products_grid_content_widget" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
            <image id="recently_viewed_products_images_names_widget" type="small_image">
                <width>75</width>
                <height>90</height>
            </image>
            <image id="recently_viewed_products_images_only_widget" type="small_image">
                <width>76</width>
                <height>76</height>
            </image>
            <image id="recently_viewed_products_list_content_widget" type="small_image">
                <width>270</width>
                <height>270</height>
            </image>
            <image id="related_products_list" type="small_image">
                <width>140</width>
                <height>140</height>
            </image>
            <image id="review_page_product_image" type="small_image">
                <width>285</width>
                <height>285</height>
            </image>
            <image id="rss_thumbnail" type="thumbnail">
                <width>75</width>
                <height>75</height>
            </image>
            <image id="sendfriend_small_image" type="small_image">
                <width>75</width>
                <height>75</height>
            </image>
            <image id="shared_wishlist_email" type="small_image">
                <width>135</width>
                <height>135</height>
            </image>
            <image id="side_column_widget_product_thumbnail" type="thumbnail">
                <width>75</width>
                <height>90</height>
            </image>
            <image id="upsell_products_list" type="small_image">
                <width>140</width>
                <height>140</height>
            </image>
            <image id="wishlist_sidebar_block" type="thumbnail">
                <width>75</width>
                <height>90</height>
            </image>
            <image id="wishlist_small_image" type="small_image">
                <width>113</width>
                <height>113</height>
            </image>
            <image id="wishlist_thumbnail" type="small_image">
                <width>240</width>
                <height>300</height>
            </image>
        </images>
    </media>
    <vars module="Magento_Catalog">

        <!-- Gallery and magnifier theme settings. Start -->
        <var name="gallery">
            <var name="nav">thumbs</var> <!-- Gallery navigation style (false/thumbs/dots) -->
            <var name="loop">true</var> <!-- Gallery navigation loop (true/false) -->
            <var name="keyboard">true</var> <!-- Turn on/off keyboard arrows navigation (true/false) -->
            <var name="arrows">true</var> <!-- Turn on/off arrows on the sides preview (true/false) -->
            <var name="caption">false</var> <!-- Display alt text as image title (true/false) -->
            <var name="allowfullscreen">true</var> <!-- Turn on/off fullscreen (true/false) -->
            <var name="navdir">horizontal</var> <!-- Sliding direction of thumbnails (horizontal/vertical) -->
            <var name="navarrows">true</var> <!-- Turn on/off on the thumbs navigation sides arrows(true/false) -->
            <var name="navtype">slides</var> <!-- Sliding type of thumbnails (slides/thumbs) -->
            <var name="transition">
                <var name="effect">slide</var> <!-- Sets transition effect for slides changing (slide/crossfade/dissolve) -->
                <var name="duration">500</var> <!-- Sets transition duration in ms -->
            </var>
            <var name="fullscreen">
                <var name="nav">thumbs</var> <!-- Fullscreen navigation style (false/thumbs/dots) -->
                <var name="loop">true</var> <!-- Fullscreen navigation loop (true/false/null) -->
                <var name="arrows">false</var> <!-- Turn on/off arrows on the sides preview in fullscreen (true/false/null) -->
                <var name="caption">false</var> <!-- Display alt text as image title in fullscreen(true/false) -->
                <var name="navdir">horizontal</var> <!--Sliding direction of thumbnails in fullscreen(horizontal/vertical)  -->
                <var name="navtype">slides</var> <!-- Sliding type of thumbnails (slides/thumbs) -->
                <var name="transition">
                    <var name="effect">dissolve</var> <!-- Sets transition effect for slides changing (slide/crossfade/dissolve) -->
                    <var name="duration">500</var> <!-- Sets transition duration in ms -->
                </var>
            </var>
        </var>

        <var name="magnifier">
            <var name="fullscreenzoom">20</var>  <!-- Zoom for fullscreen (integer)-->
            <var name="top"></var> <!-- Top position of magnifier -->
            <var name="left"></var> <!-- Left position of magnifier -->
            <var name="width"></var> <!-- Width of magnifier block -->
            <var name="height"></var> <!-- Height of magnifier block -->
            <var name="eventType">hover</var> <!-- Action that atcivates zoom (hover/click) -->
            <var name="enabled">false</var> <!-- Turn on/off magnifier (true/false) -->
        </var>

        <var name="breakpoints">
            <var name="mobile">
                <var name="conditions">
                    <var name="max-width">767px</var>
                </var>
                <var name="options">
                    <var name="options">
                        <var name="nav">dots</var>
                    </var>
                </var>
            </var>
        </var>
        <!-- end. Gallery and magnifier theme settings -->

        <var name="product_small_image_sidebar_size">100</var>  <!-- Override for small product image -->
        <var name="product_base_image_size">275</var>           <!-- Override for base product image -->
        <var name="product_base_image_icon_size">48</var>       <!-- Base product image icon size -->

        <var name="product_list_image_size">166</var>           <!-- New Product image size used in product list -->
        <var name="product_zoom_image_size">370</var>           <!-- New Product image size used for zooming -->

        <var name="product_image_white_borders">1</var>
    </vars>
    <vars module="Magento_Bundle">
        <var name="product_summary_image_size">58</var>         <!-- New Product image size used for summary block-->
    </vars>
    <vars module="Js_Bundle">
        <var name="bundle_size">1MB</var>
    </vars>
    <exclude>
        <item type="file">Lib::chartjs/Chart.min.js</item>
        <item type="file">Lib::jquery/jquery.min.js</item>
        <item type="file">Lib::jquery/jquery-ui-1.9.2.js</item>
        <item type="file">Lib::jquery/jquery.details.js</item>
        <item type="file">Lib::jquery/jquery.hoverIntent.js</item>
        <item type="file">Lib::jquery/colorpicker/js/colorpicker.js</item>
        <item type="file">Lib::requirejs/require.js</item>
        <item type="file">Lib::requirejs/text.js</item>
        <item type="file">Lib::legacy-build.min.js</item>
        <item type="file">Lib::mage/captcha.js</item>
        <item type="file">Lib::mage/dropdown_old.js</item>
        <item type="file">Lib::mage/list.js</item>
        <item type="file">Lib::mage/loader_old.js</item>
        <item type="file">Lib::mage/webapi.js</item>
        <item type="file">Lib::mage/zoom.js</item>
        <item type="file">Lib::mage/translate-inline-vde.js</item>
        <item type="file">Lib::mage/requirejs/mixins.js</item>
        <item type="file">Lib::mage/requirejs/static.js</item>
        <item type="file">Magento_Customer::js/zxcvbn.js</item>
        <item type="file">Magento_Catalog::js/zoom.js</item>
        <item type="file">Magento_Ui::js/lib/step-wizard.js</item>
        <item type="file">Magento_Ui::js/form/element/ui-select.js</item>
        <item type="file">Magento_Ui::js/form/element/file-uploader.js</item>
        <item type="file">Magento_Ui::js/form/components/insert.js</item>
        <item type="file">Magento_Ui::js/form/components/insert-listing.js</item>
        <item type="directory">Magento_Ui::js/timeline</item>
        <item type="directory">Magento_Ui::js/grid</item>
        <item type="directory">Magento_Ui::js/dynamic-rows</item>
        <item type="directory">Magento_Ui::templates/timeline</item>
        <item type="directory">Magento_Ui::templates/grid</item>
        <item type="directory">Magento_Ui::templates/dynamic-rows</item>
        <item type="directory">Magento_Swagger::swagger-ui</item>
        <item type="directory">Magento_Tinymce3::tiny_mce</item>
        <item type="directory">Lib::modernizr</item>
        <item type="directory">Lib::tiny_mce</item>
        <item type="directory">Lib::varien</item>
        <item type="directory">Lib::jquery/editableMultiselect</item>
        <item type="directory">Lib::jquery/jstree</item>
        <item type="directory">Lib::jquery/fileUploader</item>
        <item type="directory">Lib::css</item>
        <item type="directory">Lib::lib</item>
        <item type="directory">Lib::extjs</item>
        <item type="directory">Lib::prototype</item>
        <item type="directory">Lib::scriptaculous</item>
        <item type="directory">Lib::less</item>
        <item type="directory">Lib::mage/adminhtml</item>
        <item type="directory">Lib::mage/backend</item>
    </exclude>
</view>

That's all you need to create a theme. Now go to your Magento admin and log in. Then, go to Content -> Configuration. Click on Edit for the Default Store View. Now, choose your theme from the "Applied Theme" dropdown and click Save Configuration.


Understanding the Base Email Styling Files

Usually, all custom themes (like the one we just created) extend Magento's basic theme blank. So, the styling in that theme will be applied.

The code for blank is inside vendor/magento/theme-frontend-blank. All theme stylings are in web/css.

Inside blank's theme stylings, we'll focus on 3 files:

  1. web/css/source/_email-base.less: The file that holds the email stylings.
  2. web/css/source/_email-extend.less: This file allows child themes to extend the default stylings in _email-base.less and make any necessary changes needed.
  3. web/css/source/_email-variables.less: This file allows child themes to modify variables used in _email-base.less.

The most important one here is email-variables.less. We can easily modify the styles of emails just by changing the values of some variables. You can find these variables used in _email-base.less like:

  1. @font-family__base: The font family to use.
  2. @email__background-color: The entire background color for the email
  3. @email-content__background-color: The background color for the main content of the email. This is helpful if you want the content of the email to not span the entire document's width.
  4. @email-body__width: specify the width of the content of the email.

There are many other variables as well that you can use to modify emails. I suggest you take a look at web/css/source/_email-base.less to see all the variables that can be used.

By default, the emails will have a pure background color, with the content's width being 600px. It should look something like this:

Let's say we want to make the background color of the body #8ba9b7, and make the width of the content 70%.

To do that, create the file app/design/frontend/VENDOR_NAME/THEME_NAME/web/css/source/_email-variables.less with the following content:

@email__background-color: #8ba9b7;
@email-body__width: 70%;

That's all we need to do to customize the email styling! Run the following command:

bin/magento setup:static-content:deploy -f

or if you're using Grunt, run the following:

grunt refresh

Recommended Read: How to Make Your Front-End Development Faster in Magento 2 Using Grunt.

Emails now will look like this:

Next step, we'll center the footer text and change the text color to white.

All changes to the design that's not just changing the variables' values should be put inside web/css/source/_email-extend.less. So, create the file app/design/frontend/VENDOR/THEME_NAME/web/css/source/_email-extend.less.

The footer has the footer class, so we'll reference it through the selector .footer:

.footer {
	color: #fff;
    text-align: center;
}

To see the changes, compile the code as mentioned earlier.

The email will now look like this:


Using Fonts in Emails

Using fonts in emails is tricky. We'll go over how to use one of the Web Safe Fonts, as those should work on most clients. We'll be using Verdana.

To change the font, set the variable @font-family__base in _email-variables.less:

@font-family__base: 'Verdana', sans-serif;

If we compile as usual and test the email, we can see that the font has changed:

You can also use your own fonts, however, this rarely works in most email clients.

Use Own Fonts

To use your own font, first, copy the font to app/design/frontend/VENDOR/THEME_NAME/web/font/FONT where FONT is the font file. For example, Opensans.ttf.

Next, in _email-extend.less, we use the .lib-font-face mixin which receives the following parameters:

  1. @family-name: the font family name.
  2. @font-path: the path to the font family.
  3. @font-weight: the weight of the font
  4. @font-style: the style of the font
  5. @font-display: display property of the font
  6. @font-format: the extension type of the font. This is optional. If your FONT's extension is woff you don't need to pass this.
  7. @font-type: same as @font-format. When using a font format that's different than woff you need to pass both parameters.

So, this is an example of how you'll use the mixin:

.lib-font-face(
    @family-name: 'MyFont',
    @font-path: '@{baseDir}fonts/FONT',
    @font-weight: 300,
    @font-style: normal,
    @font-display: swap,
    @font-format: 'ttf',
    @font-type: 'ttf'
);

Notice that for @font-path we're using the variable baseDir which refers to app/design/frontend/VENDOR/THEME_NAME/web.

Then, in email-variables.less set the @font-family__base to your font:

@font-family__base: 'MyFont', sans-serif;

Then compile the changes and test the email, however, there's no guarantee that it will actually work.

Note About Using Google Fonts

If you want to use Google Fonts, which again is not guaranteed to work, here's a little tip on the best way to do it.

First, choose a font. For example, Odibee Sans. Choose the style you want, then copy the import link.

Open that link your browser, it will give you the CSS rules to create the font.

Copy it, then paste it in email-extend.less. Then, change the value for the @font-family_base variable in _email-variables.less to the font:

@font-family__base: 'Odibee Sans', sans-serif;

After compiling the changes as before, you can test the email and see if the font works. However, as stated earlier, this is not supported by most clients and most probably will not work. It's best to stick to websafe fonts.