Back to Top

Understanding Magento Theme : Layout customizations

Updated 13 April 2024

Hello Friends !!

In this blog, We will discuss about magento theme layout customizations related points.

So, Firstly we try to understand how we can create a custom theme in Magento 2. You can refer the our Custom Theme in Magento 2 blog to create a custom theme.

While creating a custom theme, We should always use best practices for theme development to avoid conflicts and issues with our theme after we update or upgrade our Magento instance.

You can refer our Best approach to creating a theme in Magento 2 blog to avoid conflict issues.

Searching for an experienced
Magento 2 Company ?
Find out More

Now back to our topic, for layout customizations in Magento theme, We will cover the below topics:-

1. Override the Core Magento layout

In the below example, we are overriding Magento_Catalog module’s layout/catalog_category_view.xml layout to our custom theme to remove breadcumbs from category view page.

To achieve this, We create a same layout file (catalog_category_view.xml) inside the below theme directory:-

app/design/frontend/Webkul/customtheme/Magento_Catalog/layout/

<?xml version="1.0"?>
<!--
/**
 * Webkul Software.
 *
 * @category   Webkul
 * @package    Webkul_CustomTheme
 * @author     Webkul Software Private Limited
 * @copyright  Webkul Software Private Limited (https://webkul.com)
 * @license    https://store.webkul.com/license.html
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="breadcrumbs" remove="true"/>
    </body>
</page>

Here is the result of the mentioned code. Breadcumbs has been removed from the category view page.

breadcumbs-remove

2. Override the core Magento Style

In the below example, we are overriding Magento_Catalog module’s web/css/_module.less style file to our custom theme to change the colour of add to cart button.

To achieve this, We create a same style file (_module.less) inside the below theme directory:-

app/design/frontend/Webkul/customtheme/Magento_Catalog/Web/css/

@import 'source/_extend.less';
.actions-primary{
    background-color: red;
    border:1px solid red;
    color:#fff;
}

Here is the result of the mentioned code.

Image 1:- Without overriding the css.

Theme-layouts-customizations-before-css-override

Image 2:- With overriding the css. Add to cart button colour has been changed.

Theme-layouts-customizations-after-css-override

3. Override Core Magento Template

In the below example, we are overriding Magento_Catalog module’s templates/product/list.phtml template file to our custom theme to replace add to cart text with purchase now.

To achieve this, We create a same template file (list.phtml) inside the below directory:-

app/design/frontend/Webkul/customtheme/Magento_Catalog/templates/product/

<?php
/**
 * Webkul Software.
 *
 * @category   Webkul
 * @package    Webkul_CustomTheme
 * @author     Webkul Software Private Limited
 * @copyright  Webkul Software Private Limited (https://webkul.com)
 * @license    https://store.webkul.com/license.html
 */
use Magento\Framework\App\Action\Action;

/**
 * Product list template
 *
 * @var $block \Magento\Catalog\Block\Product\ListProduct
 * @var \Magento\Framework\Escaper $escaper
 * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer
 */
?>
<?php
$_productCollection = $block->getLoadedProductCollection();
/** @var \Magento\Catalog\Helper\Output $_helper */
$_helper = $block->getData('outputHelper');
?>
<?php if (!$_productCollection->count()): ?>
    <div class="message info empty">
        <div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div>
    </div>
<?php else: ?>
    <?= $block->getToolbarHtml() ?>
    <?= $block->getAdditionalHtml() ?>
    <?php
    if ($block->getMode() === 'grid') {
        $viewMode = 'grid';
        $imageDisplayArea = 'category_page_grid';
        $showDescription = false;
        $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW;
    } else {
        $viewMode = 'list';
        $imageDisplayArea = 'category_page_list';
        $showDescription = true;
        $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::FULL_VIEW;
    }
    /**
     * Position for actions regarding image size changing in vde if needed
     */
    $pos = $block->getPositioned();
    ?>
    <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>">
        <ol class="products list items product-items">
            <?php /** @var $_product \Magento\Catalog\Model\Product */ ?>
            <?php foreach ($_productCollection as $_product): ?>
            <li class="item product product-item">
                <div class="product-item-info"
                     id="product-item-info_<?= /* @noEscape */ $_product->getId() ?>"
                     data-container="product-<?= /* @noEscape */ $viewMode ?>">
                    <?php
                    $productImage = $block->getImage($_product, $imageDisplayArea);
                    if ($pos != null) {
                        $position = 'left:' . $productImage->getWidth() . 'px;'
                            . 'top:' . $productImage->getHeight() . 'px;';
                    }
                    ?>
                    <?php // Product Image ?>
                    <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"
                       class="product photo product-item-photo"
                       tabindex="-1">
                        <?= $productImage->toHtml() ?>
                    </a>
                    <div class="product details product-item-details">
                        <?php $_productNameStripped = $block->stripTags($_product->getName(), null, true); ?>
                        <strong class="product name product-item-name">
                            <a class="product-item-link"
                               href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>">
                                <?=/* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name')?>
                            </a>
                        </strong>
                        <?= $block->getReviewsSummaryHtml($_product, $templateType) ?>
                        <?= /* @noEscape */ $block->getProductPrice($_product) ?>

                        <?= $block->getProductDetailsHtml($_product) ?>

                        <div class="product-item-inner">
                            <div class="product actions product-item-actions">
                                <div class="actions-primary">
                                    <?php if ($_product->isSaleable()):?>
                                        <?php $postParams = $block->getAddToCartPostParams($_product); ?>
                                        <form data-role="tocart-form"
                                              data-product-sku="<?= $escaper->escapeHtml($_product->getSku()) ?>"
                                              action="<?= $escaper->escapeUrl($postParams['action']) ?>"
                                              data-mage-init='{"catalogAddToCart": {}}'
                                              method="post">
                                            <?php $options = $block->getData('viewModel')->getOptionsData($_product); ?>
                                            <?php foreach ($options as $optionItem): ?>
                                                <input type="hidden"
                                                       name="<?= $escaper->escapeHtml($optionItem['name']) ?>"
                                                       value="<?= $escaper->escapeHtml($optionItem['value']) ?>">
                                            <?php endforeach; ?>
                                            <input type="hidden"
                                                   name="product"
                                                   value="<?= /* @noEscape */ $postParams['data']['product'] ?>">
                                            <input type="hidden"
                                                   name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>"
                                                   value="<?=
                                                   /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED]
                                                    ?>">
                                            <?= $block->getBlockHtml('formkey') ?>
                                            <button type="submit"
                                                    title="<?= $escaper->escapeHtmlAttr(__('Purchase Now')) ?>"
                                                    class="action tocart primary"
                                                    disabled>
                                                <span><?= $escaper->escapeHtml(__('Purchase Now')) ?></span>
                                            </button>
                                        </form>
                                    <?php else:?>
                                        <?php if ($_product->isAvailable()):?>
                                            <div class="stock available">
                                                <span><?= $escaper->escapeHtml(__('In stock')) ?></span></div>
                                        <?php else:?>
                                            <div class="stock unavailable">
                                                <span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div>
                                        <?php endif; ?>
                                    <?php endif; ?>
                                </div>
                                <?= ($pos && strpos($pos, $viewMode . '-primary')) ?
                                    /* @noEscape */ $secureRenderer->renderStyleAsTag(
                                        $position,
                                        'product-item-info_' . $_product->getId() . ' div.actions-primary'
                                    ) : '' ?>
                                <div data-role="add-to-links" class="actions-secondary">
                                    <?php if ($addToBlock = $block->getChildBlock('addto')): ?>
                                        <?= $addToBlock->setProduct($_product)->getChildHtml() ?>
                                    <?php endif; ?>
                                </div>
                                <?= ($pos && strpos($pos, $viewMode . '-secondary')) ?
                                    /* @noEscape */ $secureRenderer->renderStyleAsTag(
                                        $position,
                                        'product-item-info_' . $_product->getId() . ' div.actions-secondary'
                                    ) : '' ?>
                            </div>
                            <?php if ($showDescription): ?>
                                <div class="product description product-item-description">
                                    <?= /* @noEscape */ $_helper->productAttribute(
                                        $_product,
                                        $_product->getShortDescription(),
                                        'short_description'
                                    ) ?>
                                    <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"
                                       title="<?= /* @noEscape */ $_productNameStripped ?>"
                                       class="action more"><?= $escaper->escapeHtml(__('Learn More')) ?></a>
                                </div>
                            <?php endif; ?>
                        </div>
                    </div>
                </div>
                <?= ($pos && strpos($pos, $viewMode . '-actions')) ?
                /* @noEscape */ $secureRenderer->renderStyleAsTag(
                    $position,
                    'product-item-info_' . $_product->getId() . ' div.product-item-actions'
                ) : '' ?>
            </li>
            <?php endforeach; ?>
        </ol>
    </div>
    <?= $block->getChildBlock('toolbar')->setIsBottom(true)->toHtml() ?>
    <?php // phpcs:ignore Magento2.Legacy.PhtmlTemplate ?>
<?php endif; ?>

Here is the result of the mentioned code. Add to cart text has been replaced with Purchase Now.

Theme-layouts-customizations-template-override-1

4. Override Core Magento js

In the below example, we are overriding Magento_Checkout module’s web/js/view/payment/default.js file to our custom theme to add confirmation to place order button on click action.

To achieve this, We create a same js file (default.js) inside the below directory:-

app/design/frontend/Webkul/customtheme/Magento_Checkout/web/js/view/payment/

/**
 * Webkul Software.
 *
 * @category   Webkul
 * @package    Webkul_CustomTheme
 * @author     Webkul Software Private Limited
 * @copyright  Webkul Software Private Limited (https://webkul.com)
 * @license    https://store.webkul.com/license.html
 */

 define([
    'ko',
    'jquery',
    'uiComponent',
    'Magento_Checkout/js/action/place-order',
    'Magento_Checkout/js/action/select-payment-method',
    'Magento_Checkout/js/model/quote',
    'Magento_Customer/js/model/customer',
    'Magento_Checkout/js/model/payment-service',
    'Magento_Checkout/js/checkout-data',
    'Magento_Checkout/js/model/checkout-data-resolver',
    'uiRegistry',
    'Magento_Checkout/js/model/payment/additional-validators',
    'Magento_Ui/js/model/messages',
    'uiLayout',
    'Magento_Checkout/js/action/redirect-on-success',
    'Magento_Ui/js/modal/confirm'
], function (
    ko,
    $,
    Component,
    placeOrderAction,
    selectPaymentMethodAction,
    quote,
    customer,
    paymentService,
    checkoutData,
    checkoutDataResolver,
    registry,
    additionalValidators,
    Messages,
    layout,
    redirectOnSuccessAction,
    confirmation
) {
    'use strict';

    return Component.extend({
        redirectAfterPlaceOrder: true,
        isPlaceOrderActionAllowed: ko.observable(quote.billingAddress() != null),

        /**
         * After place order callback
         */
        afterPlaceOrder: function () {
            // Override this function and put after place order logic here
        },

        /**
         * Initialize view.
         *
         * @return {exports}
         */
        initialize: function () {
            var billingAddressCode,
                billingAddressData,
                defaultAddressData;

            this._super().initChildren();
            quote.billingAddress.subscribe(function (address) {
                this.isPlaceOrderActionAllowed(address !== null);
            }, this);
            checkoutDataResolver.resolveBillingAddress();

            billingAddressCode = 'billingAddress' + this.getCode();
            registry.async('checkoutProvider')(function (checkoutProvider) {
                defaultAddressData = checkoutProvider.get(billingAddressCode);

                if (defaultAddressData === undefined) {
                    // Skip if payment does not have a billing address form
                    return;
                }
                billingAddressData = checkoutData.getBillingAddressFromData();

                if (billingAddressData) {
                    checkoutProvider.set(
                        billingAddressCode,
                        $.extend(true, {}, defaultAddressData, billingAddressData)
                    );
                }
                checkoutProvider.on(billingAddressCode, function (providerBillingAddressData) {
                    checkoutData.setBillingAddressFromData(providerBillingAddressData);
                }, billingAddressCode);
            });

            return this;
        },

        /**
         * Initialize child elements
         *
         * @returns {Component} Chainable.
         */
        initChildren: function () {
            this.messageContainer = new Messages();
            this.createMessagesComponent();

            return this;
        },

        /**
         * Create child message renderer component
         *
         * @returns {Component} Chainable.
         */
        createMessagesComponent: function () {

            var messagesComponent = {
                parent: this.name,
                name: this.name + '.messages',
                displayArea: 'messages',
                component: 'Magento_Ui/js/view/messages',
                config: {
                    messageContainer: this.messageContainer
                }
            };

            layout([messagesComponent]);

            return this;
        },

        /**
         * Place order.
         */
        placeOrder: function (data, event) {
            var self = this;
            confirmation({
                title: 'Order Confirmation',
                content: 'Do you want to place this order?',
                actions: {
                    confirm: function () {
                        if (event) {
                            event.preventDefault();
                        }
            
                        if (self.validate() &&
                            additionalValidators.validate() &&
                            self.isPlaceOrderActionAllowed() === true
                        ) {
                            self.isPlaceOrderActionAllowed(false);
            
                            self.getPlaceOrderDeferredObject()
                                .done(
                                    function () {
                                        self.afterPlaceOrder();
            
                                        if (self.redirectAfterPlaceOrder) {
                                            redirectOnSuccessAction.execute();
                                        }
                                    }
                                ).always(
                                    function () {
                                        self.isPlaceOrderActionAllowed(true);
                                    }
                                );
            
                            return true;
                        }
                    },
                    cancel: function () {
                        return false;
                    }
                }
            });
            return false;
        },

        /**
         * @return {*}
         */
        getPlaceOrderDeferredObject: function () {
            return $.when(
                placeOrderAction(this.getData(), this.messageContainer)
            );
        },

        /**
         * @return {Boolean}
         */
        selectPaymentMethod: function () {
            selectPaymentMethodAction(this.getData());
            checkoutData.setSelectedPaymentMethod(this.item.method);

            return true;
        },

        isChecked: ko.computed(function () {
            return quote.paymentMethod() ? quote.paymentMethod().method : null;
        }),

        isRadioButtonVisible: ko.computed(function () {
            return paymentService.getAvailablePaymentMethods().length !== 1;
        }),

        /**
         * Get payment method data
         */
        getData: function () {
            return {
                'method': this.item.method,
                'po_number': null,
                'additional_data': null
            };
        },

        /**
         * Get payment method type.
         */
        getTitle: function () {
            return this.item.title;
        },

        /**
         * Get payment method code.
         */
        getCode: function () {
            return this.item.method;
        },

        /**
         * @return {Boolean}
         */
        validate: function () {
            return true;
        },

        /**
         * @return {String}
         */
        getBillingAddressFormName: function () {
            return 'billing-address-form-' + this.item.method;
        },

        /**
         * Dispose billing address subscriptions
         */
        disposeSubscriptions: function () {
            // dispose all active subscriptions
            var billingAddressCode = 'billingAddress' + this.getCode();

            registry.async('checkoutProvider')(function (checkoutProvider) {
                checkoutProvider.off(billingAddressCode);
            });
        }
    });
});

Here is the result of the mentioned code. The confirmation has been added on place order button click.

Theme-layouts-customizations-override-js

That’s all about the Magento Theme : Layout customizations. Hope this will be helpful.

If you have any questions please comment below, and we will try to respond to you.

Thanks! 🙂

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home