How to Build a Theme in Magento 2

When building a custom theme for Magento, it’s important to follow best practices for how the system is designed to be extended. Magento 2’s frontend is significantly different than that of Magento 1, so even for seasoned Magento developers, there are plenty of things worth noting.
This article is not an exhaustive step-by-step guide to frontend customization. It’s primarily meant to outline the basics for how the system is structured and how to best extend it, as well as to serve as a reference for various theme topics. In general, Magento’s official DevDocs are a good resource for specific tasks.

I’ll start by recommending a global-first approach to theme building. By implementing key branding elements such as logo, typography, and theme colors, we’ll stick to the critical path and create a foundation upon which the rest of the theme can be built. For example, when we’re styling our product listing page, we shouldn’t have to worry about how the add-to-cart buttons look, because they should already match the rest of the buttons on the site, which should all be styled at a global scope.
We’ll outline several areas of theme building below (such as logo, typography, icons, and global styles) that can be used to quickly get a site looking distinctly branded.

Creating a new theme

For a new theme, we’ll need to create registration.php and theme.xml in our theme directory, and then run $ bin/magento setup:upgrade.

registration.php registers the theme as a system component at the specified location (as with modules).

Example:

<?php
use MagentoFrameworkComponentComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/<myVendorName>/<myCustomTheme>', __DIR__);

theme.xml defines basic theme configuration, including at least the title and usually the parent theme that’s being extended.

Example:

<theme xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>My Custom Theme</title>
    <parent>Magento/blank</parent>
    <media>
        <preview_image>media/preview.jpg</preview_image>
    </media>
</theme>

See more in DevDocs under ‘Create a new storefront theme’.

Note: It’s recommended that all custom themes extend Magento/blank instead of Magento/luma, since the Luma theme is only intended to be an example.

The logo is an important part of the theme, since it’s the primary identity of the site that first greets the user. We strongly recommend using an SVG vector image whenever possible, since it will scale to any screen resolution or zoom level.

Magento 2 uses an SVG logo by default, and it will find yours automatically if you put it in your theme directory at: web/images/logo.svg.

You may also need to specify the dimensions in: Magento_Theme/layout/default.xml.

Example:

<?xml version="1.0"?>
<page xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="logo">
            <arguments>
                <!-- Set numbers as needed -->
                <argument name="logo_width" xsi_type="number">180</argument>
                <argument name="logo_height" xsi_type="number">75</argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Product Images

Images for catalog products can be sized and re-sized in Magento without having to change any styles. Default configuration is found in the Magento Blank theme, in etc/view.xml. Configuration can be customized by redefining values in etc/view.xml in our theme directory.

Example:

Let’s set different widths and heights for images in the grid view and list view on the category page:

<?xml version="1.0"?>
<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="category_page_grid" type="small_image">
                <width>200</width>
                <height>200</height>
            </image>
            <image id="category_page_list" type="small_image">
                <width>230</width>
                <height>160</height>
            </image>
        </images>
    </media>
</view>

There’s also a section in etc/view.xml for configuring several aspects of the image gallery on the product detail page.

Example:

Let’s set the gallery thumbnails to display vertically instead of horizontally:

<?xml version="1.0"?>
<view xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:Config/etc/view.xsd">
    <vars module="Magento_Catalog">
        <var name="gallery">
            <var name="navdir">vertical</var> <!-- Sliding direction of thumbnails (horizontal/vertical) -->
            <var name="fullscreen">
                <var name="navdir">vertical</var> <!--Sliding direction of thumbnails in fullscreen (horizontal/vertical) -->
            </var>
        </var>
    </vars>
</view>

See more in DevDocs under ‘Configure images properties for a theme’.

UI Library

The Magento 2 theme was built using a system of LESS mixins (collectively the “UI Library”), which are located in: /lib/web/css/source/lib. Each of the LESS files has extensible mixins that are used to define the look of global elements in Magento Blank, using default variables defined in: /lib/web/css/source/lib/variables.

Example:

  • Default button styles come from the .lib-button() mixin in: /lib/web/css/source/lib/_buttons.less.
  • The variables used to set the default values for .lib-button() come from: /lib/web/css/source/lib/variables/_buttons.less.

These mixins can be called (or redefined if necessary) when building a custom theme, but many of the mixins are flexible enough that a lot of global styles can (and should) be customized simply by redefining the default variables. By redefining a value (like a button background color) at a low level, we can recompile styles one time and immediately see the change reflected across the entire site.

The UI library has some built-in documentation in /lib/web/css/source/docs. This can be viewed as web content by simply moving the entire /lib/web/css/source/docs directory into /pub/static. Then just visit “<your_magento_base_url>/static/docs/index.html” in a web client. It’s worth reading through the introduction page of this documentation, as well as at least skimming through each of the pages linked in the ‘files’ dropdown at the top, to familiarize yourself with how the mixins are used and what the default variables are. This can also be helpful to better understand the approach that was used to build the Magento Blank theme, and to understand the file structure in /lib/web/css/source/lib.

Styling (CSS and LESS)

Style preprocessing (LESS)

Magento 2 has a lot of LESS files scattered throughout the codebase by default, which can initially be pretty confusing. This assortment is due to the modularity of the system and the usage of /var/view_preprocessed for compilation. This allows styles to be separated in the system (by library, theme, and module) but merged in /var/view_preprocessed during compilation, to enable the use of relative import paths. From there, the CSS is compiled into /pub/static/frontend to be served to the user agent.

The flexible import structure can be seen by observing a specific example in the Magento Blank theme, in web/css/styles-m.less:

  • Line 31 shows this import directive: @import 'source/lib/_responsive.less'.
  • No file exists in the theme at web/css/source/lib/_responsive.less, the relative path this would seem to point to.
  • Where this file does exist is in /lib/web/css/source/lib/_responsive.less.

How this works in action can be seen in /var/view_preprocessed once static asset deployment is performed.

  • /var/view_preprocessed/css/frontend/Magento/blank/en_US/css/styles-m.less contains the same import directive.
  • The relative path of the import now correctly refers to /var/view_preprocessed/css/frontend/Magento/blank/en_US/css/source/lib/_responsive.less, which can be seen to be identical to the source file in /lib.

Keeping this construct in mind can make it easier to understand the LESS file structure between library and modules.

See more in DevDocs under ‘CSS preprocessing’.

Usage of LESS files

Besides the files in /lib (mentioned in the “M2 UI Library” section above), which you may find cause to reference often during a theme build, some other key files to know how to use are: _theme.less, _module.less, _extend.less, and _styles.less. Each of these will be included by the system automatically (without a custom @import directive), from various locations.

  • _theme.less is for redefining existing variables, as well as for defining new variables and/or mixins that apply to your theme globally.
  • _module.less is intended to define original styles for a specific extension. Files with this name will not usually be included in a theme.
  • _extend.less is used in multiple places within the theme to modify or extend existing styles for specific extensions.
  • _styles.less is primarily reserved for imports and can be pulled forward from the parent theme to add new custom LESS files.

I’ve spared most of the details for the usage of these files, as I recommend checking out Classy Llama’s article ‘A Look at CSS and Less in Magento 2‘ for more information.

Grunt

When Magento 2 is in production mode, if styles are changed, they must be recompiled by running $ bin/magento setup:static-content:deploy. This is a long process that goes through every static file for all themes. When the system is in default or developer mode, static files are automatically generated by the system when needed, which is also a long process, and once styles have been generated, changes are not picked up automatically. Styles can be recompiled manually by re-deploying static content, but the most efficient method is to use a task runner, especially when doing active theme development. Magento 2 includes built-in support for compiling LESS with Grunt. The process is outlined in DevDocs under ‘Compile LESS with Grunt’, but here are a few things that are helpful to remember:

  • Before running the npm installer, remove the ‘.sample’ suffix from Gruntfile.js.sample and package.json.sample in your site root.
  • There are a couple things to note under “Installing and configuring Grunt” in the linked doc above.
    • The <theme> value should be whatever you prefer to use on the command line when running Grunt.
    • The “name” value must match the registered theme name as it appears in the registration.php file for your theme, in the string beginning with “frontend/”. (For example, in the registration.php file for Magento Blank, the 2nd parameter passed to register() is “frontend/Magento/blank”, so the “name” value in /dev/tools/grunt/configs/themes.js is “Magento/blank”.)
  • Any time that LESS files are added or deleted, run grunt exec:<theme>, followed by grunt less:<theme>.
  • If changes were made only to existing LESS files, you should only have to run grunt less:<theme>.
  • You can use grunt watch and grunt watch:<theme> to detect changes to LESS files and automatically recompile styles.
    • If you have LiveReload installed, this can be a convenient way to avoid having to refresh your browser.

Fonts/Typography

Default font families are declared in the Magento Blank theme, in web/css/source/_typography.less. We can define our own custom fonts using the same structure by creating and including our own _typography.less file in our theme. The @font-path passed to the .lib-font-face() mixin simply has to match the path to the font files within our theme. The file extensions will be added by the system on the frontend as needed. (It’s recommended that the five file extensions used by Magento be included to ensure that our fonts will render on all browsers.)

Example:

  • Let’s look at: web/css/source/_typography.less in the Magento Blank theme.
  • On line 13, we see this: @font-path: '@{baseDir}fonts/opensans/light/opensans-300'.
  • This declaration corresponds to the 5 files at: /lib/web/fonts/opensans/light/opensans-300 (.eot, .svg, .ttf, .woff, and .woff2).
  • In a similar way, to define “MyFont” as the default for our site, but without disturbing the core font declarations that we might want to use elsewhere, we could do the following:
    1. Create the following file in our theme directory: web/css/source/custom/_typography.less
    2. Add the following line to our _styles.less file:
      @import 'source/custom/_typography.less';
      
    3. Call the .lib-font-face() mixin in our new file, with appropriate parameters:
      .lib-font-face(
          @family-name: @font-family-name__base,
          @font-path: '@{baseDir}fonts/MyFont',
          @font-weight: 400,
          @font-style: normal
      );
      
    4. Redefine the name of the base font family in _theme.less:
      @font-family-name__base: 'my-font';
      
    5. Add the font files in the 5 recommended file types in our theme directory at: web/fonts/MyFont.

Note: If you don’t have access to all 5 file types, Transfonter is a great resource for converting between TTF, SVG, EOT, WOFF, and WOFF2 as needed.

Icons

Icons in Magento 2 are implemented using icon fonts, which means they’re vector glyphs that are 100% scalable for all screen resolutions and zoom levels. Colors and sizes can easily be adjusted with CSS properties, just like standard text. Like other elements of the M2 UI library (discussed above in the “M2 UI Library” section), icon variables and mixins can be found in: /lib/web/css/source/lib. The icon fonts will need to be declared and loaded just like other fonts in the system, as outlined in the previous “Fonts” section. For a basic introduction to using icon fonts in Magento 2, I suggest this article by Classy Llama: ‘Icon Fonts in Magento 2: A Foundation’.

Favicons

To add a basic favicon like what most browser tabs display, we can simply add Magento_Theme/web/favicon.ico to our theme directory, and the Magento system will find it automatically.
However, favicons these days have many proprietary implementations, so a tool like RealFaviconGenerator is very helpful for getting all favicons for many different systems. I’ll be using their service as an example for how to implement custom favicons in Magento 2 for multiple web clients and platforms:

  1. Go to realfavicongenerator.net.
  2. Click ‘Select your Favicon picture’ and upload an image for your primary favicon (preferably an SVG vector image).
  3. On the next page, go through each of the options to configure and preview how your icon will appear on various platforms.
    1. Note that in each case, there’s a ‘Dedicated picture’ option where you can upload a separate image if you don’t like how the main one looks.
  4. Under ‘Favicon Generator Options’ at the bottom, select the option saying “I cannot or I do not want to place favicon files at the root of my web site…”
    1. Enter the following path in the text input: images/favicons
  5. Click ‘Generate your Favicons and HTML code’.
  6. On the next page, download the package as instructed in step 1.
  7. Extract the package in your theme directory at: web/images/favicons (instead of using the path given in step 2).
  8. Move the favicon.ico file to Magento_Theme/web (since that’s where Magento looks for it by default).
  9. To insert the provided code in the head section of each page:
    1. Create Magento_Theme/layout/default_head_blocks.xml in your theme directory.
    2. Put the given code inside page > head.
    3. Change each href attribute to a src attribute.
    4. Remove the color attribute from the safari-pinned-tab.svg link (since this is not valid XML in Magento).
    5. Remove the line that links the favicon.ico file (since this is already included directly in Magento_Theme/web).
    6. Convert closing tags to />, to make XML valid for Magento.

Example:

<?xml version="1.0"?>
<page xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <link rel="apple-touch-icon" sizes="180x180" src="images/favicons/apple-touch-icon.png" />
        <link rel="icon" type="image/png" sizes="32x32" src="images/favicons/favicon-32x32.png" />
        <link rel="icon" type="image/png" sizes="16x16" src="images/favicons/favicon-16x16.png" />
        <link rel="manifest" src="images/favicons/manifest.json" />
        <link rel="mask-icon" src="images/favicons/safari-pinned-tab.svg" />
        <meta name="msapplication-config" content="images/favicons/browserconfig.xml" />
        <meta name="theme-color" content="#ffffff" />
    </head>
</page>

See more in DevDocs under ‘Adding custom favicons’.

Other helpful tips

This section contains some tips and to-dos that I’ve consistently found helpful when creating new themes. (Your method may vary, and some of the tips may become obsolete as Magento gets updated, so take it for what it’s worth.)

Directories to exclude

Due to its many application layers, code tests, and dynamically generated files, Magento 2 has a lot of areas that aren’t directly relevant to a standard development workflow. I’ve personally found it helpful to exclude the following project directories in my IDE, to greatly reduce irrelevant search results and excess indexing:

  • /bin, /dev, /setup, /update
  • /artifact, /generated, /var
  • /lib/web/css/docs
  • /pub/static
  • /vendor/magento/magento2-b2b-base
  • /vendor/magento/magento2-base
  • /vendor/magento/magento2-ee-base
  • /vendor/magento/theme-frontend-luma

Note: In PhpStorm, this can be done in the “Project” Tool Window [⌘1]. Just right-click the directory you want to exclude, hover over “Mark Directory as,” and choose “Excluded.”
(Alternatively, go to Preferences > Directories > Exclude files, and enter the following pattern: `artifacts;bin;dev;generated;setup;update;var;docs;static;magento2-base;magento2-ee-base;magento2-b2b-base;theme-frontend-luma;Test`)

Changes to default secondary color

[NOTE: As of Magento 2.3.1, the changes in this section are no longer necessary, due to Magento pull request #19467 being accepted and merged.]

Prior to version 2.3.1, Magento Blank was full of places where @color-orange-red1 (or @color-orange-red2) was explicitly set as a highlight color for active elements. If your theme colors don’t happen to include bright orange (which seems likely), but you’ve already done your due diligence during global styling to define things like @active__color appropriately, you may be dismayed to be browsing around your nicely themed site and suddenly find that notorious bright orange color still lurking around. That happened to me plenty of times during my first few theme builds, so I collected this list of variables to redefine from the start:

  • @checkout-progress-bar-item__active__background-color
  • @checkout-shipping-item__active__border-color
  • @navigation-level0-item__active__border-color
  • @submenu-item__active__border-color
  • @navigation-desktop-level0-item__active__border-color
  • @submenu-desktop-item__active__border-color
  • @account-nav-current-border-color
  • @collapsible-nav-current-border-color
  • @rating-icon__active__color

(In general, I’ve found that @active__color is the most semantic value for most of these, but your usage may vary.)

Conclusion

There are a lot of moving parts in the theme process for Magento 2. Even for developers experienced with Magento 1, enough has changed that it can be difficult to understand how to work with the system properly. I hope that this guide provides some clarity and helpful practices to assist you in your next theme build on Magento.

Share it

Topics

Related Posts

Google and Yahoo Have New Requirements for Email Senders

What ROAS Really Means

Everything You Need to Know About Updating to Google Analytics 4

Contact Us