Custom Magento 2 Theme Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Custom Magento 2 Theme Development
Complex
~2-4 weeks
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Development of Custom Magento 2 Theme

A Magento 2 theme is a set of XML configurations, PHTML templates, Less/CSS and JavaScript that determines the appearance and behavior of the frontend. Unlike Shopify, there is no visual editor here — development is done with files with an understanding of the theme inheritance system and layout XML.

Theme Inheritance System

Magento themes are built on an inheritance chain. A custom theme doesn't copy all files from the base theme — it only overrides the necessary ones. Example of an inheritance chain:

Magento/blank
    └── Magento/luma
        └── MyCompany/mytheme (custom theme)

During rendering, Magento looks for a template first in the custom theme, then goes up the chain to the base. This allows you to change one file without touching the others.

Theme File Structure

app/design/frontend/MyCompany/mytheme/
├── etc/
│   └── view.xml          # image configuration (sizes, quality)
├── i18n/
│   └── en_US.csv         # string translations
├── media/
│   └── preview.jpg       # preview in Admin
├── registration.php      # theme registration
├── theme.xml             # metadata and parent
├── web/
│   ├── css/
│   │   ├── source/
│   │   │   ├── _theme.less       # theme variables
│   │   │   └── _extend.less      # base CSS extensions
│   │   └── _module.less
│   ├── fonts/
│   ├── images/
│   └── js/
│       └── theme.js
└── Magento_Catalog/              # module overrides for Catalog
    ├── layout/
    │   └── catalog_product_view.xml
    └── templates/
        └── product/
            └── view/
                └── attributes.phtml

theme.xml:

<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
    <title>MyCompany Theme</title>
    <parent>Magento/luma</parent>
    <media>
        <preview_image>media/preview.jpg</preview_image>
    </media>
</theme>

registration.php:

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::THEME,
    'frontend/MyCompany/mytheme',
    __DIR__
);

Layout XML — Foundation of the Rendering System

Layout XML manages page structure: which blocks are where, which templates they use. Without understanding Layout, you cannot properly change the page structure.

Types of layout files:

  • default.xml — applies to all pages
  • catalog_product_view.xml — product page
  • catalog_category_view.xml — category page
  • checkout_cart_index.xml — shopping cart
  • cms_index_index.xml — homepage (CMS)

Example — adding a custom block to a product page:

<!-- app/design/frontend/MyCompany/mytheme/Magento_Catalog/layout/catalog_product_view.xml -->
<?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>
        <!-- Remove standard comparison block -->
        <referenceBlock name="product.info.addtocart.additional" remove="true"/>

        <!-- Add warranty block after price -->
        <referenceContainer name="product.info.main">
            <block class="MyCompany\Catalog\Block\Product\Warranty"
                   name="product.warranty.info"
                   template="MyCompany_Catalog::product/warranty.phtml"
                   after="product.info.price"/>
        </referenceContainer>

        <!-- Move reviews above tabs -->
        <move element="product.info.details" destination="content" before="product.info.media"/>
    </body>
</page>

PHTML Templates

Templates are PHP files with HTML markup. Overriding the standard price template:

<?php
// app/design/frontend/MyCompany/mytheme/Magento_Catalog/templates/product/price/final_price.phtml
/** @var \Magento\Catalog\Block\Product\View\Description $block */
/** @var \Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability $compareAvailability */

$priceType = $block->getPriceType('final_price');
$id = $block->getPriceId() ?: 'product-price-' . $block->getProduct()->getId();
?>

<span class="price-wrapper price-final_price"
      data-price-amount="<?= $block->escapeHtmlAttr($priceType->getAmount()->getValue()) ?>"
      data-price-type="finalPrice">

    <?php if ($block->getProduct()->hasCustomOptions()): ?>
        <span class="price-label"><?= $block->escapeHtml(__('As low as')) ?></span>
    <?php endif; ?>

    <span id="<?= $block->escapeHtmlAttr($id) ?>" class="price-container">
        <span class="price">
            <?= $block->renderAmount($priceType->getAmount(), ['display_label' => false]) ?>
        </span>
    </span>
</span>

Less/CSS System

Magento 2 uses Less with a preprocessor pipeline. Base variables are overridden in web/css/source/_theme.less:

// Brand colors
@color-primary: #1a73e8;
@color-primary-darker: #1557b0;
@color-secondary: #ff6b35;

// Typography
@font-family-name__base: 'Inter';
@font-size-base: 16px;
@line-height-base: 1.6;
@font-weight__regular: 400;
@font-weight__bold: 700;

// Buttons
@button__background: @color-primary;
@button__border: 1px solid @color-primary;
@button__color: #ffffff;
@button__hover__background: @color-primary-darker;

// Header
@header__background-color: #ffffff;
@header-icons-color: @color-primary;

// Breakpoints
@screen__m: 768px;
@screen__l: 1024px;
@screen__xl: 1440px;

Extensions in _extend.less:

// Import custom font
@font-face {
    font-family: 'Inter';
    src: url('../fonts/Inter-Regular.woff2') format('woff2'),
         url('../fonts/Inter-Regular.woff') format('woff');
    font-weight: 400;
    font-display: swap;
}

// Custom product card style
.product-item {
    border: 1px solid @color-border;
    border-radius: 8px;
    transition: box-shadow 0.2s ease;

    &:hover {
        box-shadow: 0 4px 20px rgba(0,0,0,0.12);
    }

    &-photo {
        border-radius: 8px 8px 0 0;
        overflow: hidden;
    }
}

JavaScript in Theme

Magento 2 uses RequireJS to load modules. Configuration in requirejs-config.js:

// web/requirejs-config.js
var config = {
    map: {
        '*': {
            // Replace standard component
            'Magento_Catalog/js/product/view/product-info': 'MyCompany_Catalog/js/product-info-extended',
        }
    },
    config: {
        mixins: {
            'Magento_Checkout/js/view/minicart': {
                'MyCompany_Checkout/js/view/minicart-mixin': true
            }
        }
    }
};

Mixin to extend existing component without replacement:

// web/js/view/minicart-mixin.js
define(['jquery', 'mage/utils/wrapper'], function ($, wrapper) {
    'use strict';

    return function (Component) {
        return Component.extend({
            // Method override
            getCartParam: wrapper.wrap(Component.prototype.getCartParam, function (original, name) {
                if (name === 'summary_count') {
                    // Add custom count logic
                    return this.cartData()[name] || 0;
                }
                return original(name);
            })
        });
    };
});

Image Configuration

Image sizes are set in etc/view.xml:

<vars module="Magento_Catalog">
    <var name="product_image_white_borders">1</var>
</vars>

<images module="Magento_Catalog">
    <image id="product_page_image_large" type="image">
        <width>1200</width>
        <height>1200</height>
    </image>
    <image id="category_page_grid" type="image">
        <width>400</width>
        <height>400</height>
        <constrain>true</constrain>
    </image>
    <image id="product_thumbnail_image" type="thumbnail">
        <width>80</width>
        <height>80</height>
    </image>
</images>

Static Content Deployment

# Deploy for specific theme and locale
bin/magento setup:static-content:deploy en_US \
    --theme MyCompany/mytheme \
    --jobs=4 \
    -f

# During development — symlinks instead of copying
bin/magento dev:source-theme:deploy \
    --locale en_US \
    --theme MyCompany/mytheme \
    css/styles-m css/styles-l

# Manual Less compilation (for fast iteration)
grunt watch

Hyva — Alternative to Luma

Hyva Themes — modern stack for Magento 2 themes: Tailwind CSS + Alpine.js + server-side rendering through Magento layout. Instead of complex RequireJS + KnockoutJS + Less — simple, fast and understandable stack.

Commercial license ($1000/site). Delivers Lighthouse 90+ without additional optimization. Development is 2–3 times faster compared to Luma.

Timeline

Custom theme based on Luma/Blank per ready design (10–20 page types): 5–8 weeks. Hyva theme with same complexity: 3–5 weeks. Customization of existing theme (few templates, branding): 1–2 weeks.