Magento 2 Layout XML: A Practical Guide for Developers
Layout XML is the part of Magento 2 that decides which blocks render on a page, in which order, inside which containers and with which templates. Almost every frontend task touches it, from adding a block to the product page to removing something a third party module injected into the header. It is also the part where I see the most copy and paste without understanding, which leads to pages that break on the next upgrade.
This guide covers how layout XML works in Magento 2.4.8, the instructions you will use every week, and the habits that keep layout changes small and safe.
Where layout files live
A module ships its layout files in view/frontend/layout/ or view/adminhtml/layout/. A theme ships them under the module folder it targets, for example app/design/frontend/Vendor/theme/Magento_Catalog/layout/. The file name is the layout handle. When Magento builds a page, it collects every file with a matching handle from all enabled modules and from the theme chain, then merges them into one tree.
Some handles you will use often:
defaultloads on every page.catalog_product_viewloads on every product page. Magento also adds more specific handles such ascatalog_product_view_type_configurableandcatalog_product_view_id_42.catalog_category_viewloads on category pages, withcatalog_category_view_type_layeredfor anchor categories.cms_index_indexis the home page,cms_page_viewis any other CMS page.checkout_cart_indexandcheckout_index_indexare the cart and checkout.
The handle for any controller follows the pattern routeid_controller_action. If you are not sure which handles a page loads, log the result of $this->getLayout()->getUpdate()->getHandles() from a block during development.
Page layouts and page configuration
There are three kinds of layout files, and mixing them up is a common source of confusion.
- Page layouts live in
page_layout/and define the skeleton:empty,1column,2columns-left,2columns-rightand3columns. They only contain containers. - Page configuration files live in
layout/and are the ones you edit most. They can set the page layout and change theheadandbody. - Generic layouts are used for AJAX responses and some admin parts. They have a
layoutroot element instead ofpage.
A typical page configuration file looks like this:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
layout="1column"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<css src="Vendor_Module::css/product-extra.css"/>
<remove src="Some_Module::js/legacy-tracker.js"/>
</head>
<body>
<referenceContainer name="product.info.main">
<block class="Magento\Framework\View\Element\Template"
name="vendor.product.delivery"
template="Vendor_Module::product/delivery.phtml"
after="product.info.price"/>
</referenceContainer>
</body>
</page>
The layout attribute on page sets the page layout for that handle. The admin can also set a layout per CMS page, category or product, and that choice wins over the XML for that entity.
Containers and blocks
A container is a structural element with no PHP class and no template. It renders its children in order and can wrap them in an HTML tag. A block is a PHP object, usually with a template, that produces HTML.
<container name="vendor.usp.wrapper" htmlTag="div" htmlClass="usp-bar" label="USP Bar">
<block class="Magento\Cms\Block\Block" name="vendor.usp.cms">
<arguments>
<argument name="block_id" xsi:type="string">usp-bar</argument>
</arguments>
</block>
</container>
A container without children and without htmlTag renders nothing, which is useful. You can add empty containers as extension points and they cost nothing on pages where nobody uses them.
For new blocks, use Magento\Framework\View\Element\Template as the class and put logic in a view model. Writing a custom block class only to expose a few methods to a template is an older pattern that makes testing and reuse harder.
View models
A view model is a plain class that implements Magento\Framework\View\Element\Block\ArgumentInterface. You pass it as an argument and fetch it in the template.
<referenceBlock name="vendor.product.delivery">
<arguments>
<argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\Delivery</argument>
</arguments>
</referenceBlock>
<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Vendor\Module\ViewModel\Delivery $viewModel */
$viewModel = $block->getData('view_model');
?>
<p class="delivery-estimate"><?= $escaper->escapeHtml($viewModel->getEstimateText()) ?></p>
The view model gets its dependencies through the constructor like any other class, so it can be unit tested without rendering anything.
The instructions you use every week
referenceBlock and referenceContainer
These target an element declared somewhere else so you can add children, change arguments or change attributes. If the referenced name does not exist on that page, Magento ignores the instruction without an error. That is convenient, but it also means a typo fails silently, so check the result in the browser.
remove and display
<referenceBlock name="product.info.mailto" remove="true"/>
<referenceBlock name="catalog.compare.sidebar" display="false"/>
remove="true" takes the element and its children out of the tree. display="false" keeps it in the tree but does not render it, so other layout files can still reference it or move its children out. When another module moves something into a block you removed, the moved element disappears with it. If that bites you, switch to display="false".
move
<move element="product.info.stock.sku" destination="product.info.main" before="product.info.price"/>
move changes the parent and position of an element. Use before and after with an element name, or - to place it first or last. Do not remove a block in one file and redeclare it in another just to change its position. That breaks every other module that references the original.
update
<update handle="customer_account"/> pulls in all instructions of another handle. Customer account pages use it to share the sidebar navigation. It is a clean way to share a layout between your own controllers.
Arguments and ifconfig
Arguments support several xsi:type values: string, boolean, number, array, object, url, helper and options. Strings can be translated with translate="true".
Blocks can be shown conditionally with ifconfig, which reads a boolean config path:
<block class="Magento\Framework\View\Element\Template"
name="vendor.banner"
template="Vendor_Module::banner.phtml"
ifconfig="vendor_module/general/enabled"/>
Overriding vs extending layout files
A theme file with the same name as a module file extends it. Both are merged, and the theme instructions are applied after. This is what you want almost every time.
Magento also supports full overrides. A file in Vendor/theme/Magento_Catalog/layout/override/base/catalog_product_view.xml replaces the module file entirely, and override/theme/Magento/luma/ replaces a parent theme file. I avoid this. You lose every fix the module ships in that file on the next upgrade, and nothing warns you.
Cacheable blocks and full page cache
A block can be declared with cacheable="false". This does not make only that block uncached. It makes the whole page uncacheable for the full page cache, on every page where that handle loads. One such block in default.xml turns off full page cache for the entire store. It is one of the first things I search for in a performance audit:
grep -rn 'cacheable="false"' app/code app/design vendor --include=*.xml | grep -v '/Test/'
Customer specific content should come from customer data sections loaded with JavaScript, so the HTML stays the same for every visitor and Varnish can serve it.
Debugging layout
- Clear the layout and page cache after changes:
bin/magento cache:clean layout full_page. - Turn on template hints with
bin/magento dev:template-hints:enableto see which template renders each part of the page. - Validate XML against the schema in your IDE. PhpStorm resolves the
urn:magentopaths after you runbin/magento dev:urn-catalog:generate .idea/misc.xml. - When a block does not appear, check that the parent container exists on that handle and that no other file removes it.
If you are building a theme from scratch, the child theme guide shows where these files go in a real theme folder.
FAQ
What is the difference between a block and a container in Magento 2?
A container has no class and no template. It only groups children and can wrap them in an HTML tag. A block is a PHP object, usually rendering a template, that produces the actual HTML.
Why does my referenceBlock change have no effect?
The name is wrong, the block is not part of that handle, or another layout file applied later removes or changes it. Magento ignores references to missing elements without an error, so check the name and the handles that load on that page.
Should I use remove="true" or display="false"?
Use display="false" when other modules might reference the block or move children into it. Use remove="true" when you want the element and all its children gone and you know nothing depends on it.
Does cacheable="false" only affect one block?
No. It marks the whole page as uncacheable for full page cache. Load customer specific data with customer data sections instead.
I am available for freelance Magento and Adobe Commerce work, including theme and frontend fixes. If you need help with this, get in touch.
Related articles
How to Create a Magento 2 Child Theme Step by Step
Create a Magento 2 child theme of Luma or Blank: theme.xml, registration.php, composer.json, view.xml, logo, template overrides, applying and deploying it.
Magento 2 LESS Customization: _theme.less, _extend.less and More
How to customize Luma styles with LESS in Magento 2: variables in _theme.less, additions in _extend.less, module overrides, media queries and Grunt compiling.
Writing Alpine.js Components in Hyvä: Patterns That Scale
How to write Alpine.js components on Hyvä: passing PHP data, view models, customer data events, form keys, component events, CSP and keeping templates clean.
Hyvä Compatibility Modules: Make Luma Extensions Work
How Hyvä compatibility modules work: the CompatModuleRegistry, rewriting extension templates with Alpine.js, hyva_ layout handles and the Luma fallback.