The Magento checkout is the most JavaScript-heavy page in the platform: a single-page KnockoutJS application embedded in a PHP-rendered page, configured by one of the largest XML files in the codebase. Customising it safely requires understanding how the pieces connect - otherwise every change feels like defusing a bomb. Here is the map.
The Big Picture
The checkout page renders a mostly-empty PHP page, then bootstraps a JS component tree:
checkout_index_index.xmldeclares a giant nested JS layout underjsLayout- Magento merges all modules’ declarations into one JSON config
- RequireJS instantiates each declared component (
Magento_Ui/js/form/element/*and checkout-specific components) - Components bind to
.htmltemplates via Knockout observables Magento_Customer/js/customer-dataprovides cart, customer and checkout state as reactive “sections”
The upshot: checkout structure is configuration, not markup. You rearrange the checkout by editing jsLayout XML, and you add behaviour by declaring new components or extending existing ones.
jsLayout Surgery
Every step, field and block in the checkout is a named node in the tree:
<item name="shipping-step" xsi:type="array">
<item name="children" xsi:type="array">
<!-- shipping address form, shipping methods, ... -->
</item>
</item>
To move, hide or add a field, you reference the node path in your own module’s checkout_index_index.xml and set item properties: visible, sortOrder, or a whole new component. Always extend the tree declaratively this way - overriding the entire file is how upgrade conflicts are born.
A Custom Component
Adding UI means declaring a component with a JS view model and an .html template:
<item name="delivery-note" xsi:type="array">
<item name="component" xsi:type="string">Acme_Checkout/js/view/delivery-note</item>
<item name="sortOrder" xsi:type="string">25</item>
</item>
define(['uiComponent', 'ko'], function (Component, ko) {
'use strict';
return Component.extend({
defaults: {
template: 'Acme_Checkout/delivery-note',
note: ko.observable('')
}
});
});
Knockout bindings in the template (data-bind="value: note") wire the DOM to the observables. Changes propagate automatically - that reactivity is the whole point of the architecture.
customer-data and the Section Lifecycle
customer-data is the checkout’s state backbone: sections like cart, customer and checkout-data are fetched from the server, cached in localStorage, and invalidated by POST requests (the server sends an X-Requested-With-driven section list to refresh). If your custom step shows stale data, the answer is usually customerData.reload(['cart']) or declaring your own section via etc/frontend/sections.xml.
Staying Sane
- Debug with
require('uiRegistry')- every checkout component is inspectable live - Never edit
checkout_index_index.xmlwholesale; merge only your nodes - Put logic in view models, not templates
- Test on slow mobile: the checkout’s JS weight is why Hyva replaced it wholesale, and why every component you add should earn its bytes
The checkout is complex because the problem is complex. Respect the component tree and it behaves; fight it with jQuery spaghetti and every upgrade becomes a rewrite.