Customising the Magento 2 checkout is one of the most requested – and most feared – development tasks. The checkout is a JavaScript-heavy single-page application built on Knockout.js and RequireJS. Adding custom fields, new steps, or modifying the order of existing ones requires understanding the JS architecture alongside the PHP backend. I show a complete example: custom delivery note field from frontend to Order.
Checkout architecture overview
Magento 2 checkout has two main steps: Shipping and Payment. The frontend is driven by:
- JS layout –
checkout_index_index.xmldefines the component tree - Knockout.js components – each step/field is a JS component
- RequireJS mixins – extend existing components without replacing them
- PHP quote/order persistence – custom data saved via extension attributes
Step 1 – Add custom field to shipping step (JS mixin)
<!-- view/frontend/requirejs-config.js equivalent in etc/view/frontend/requirejs-config.js -->
// view/frontend/web/js/action/set-shipping-information-mixin.js
// Mixin intercepts the "proceed to payment" action and adds our custom data
define(['mage/utils/wrapper', 'Magento_Checkout/js/model/quote'], function (wrapper, quote) {
'use strict';
return function (setShippingInformationAction) {
return wrapper.wrap(setShippingInformationAction, function (originalAction) {
var shippingAddress = quote.shippingAddress();
// Read our custom field value from the form
var deliveryNote = document.getElementById('delivery_note')?.value || '';
if (shippingAddress && deliveryNote) {
shippingAddress.extensionAttributes = shippingAddress.extensionAttributes || {};
shippingAddress.extensionAttributes.delivery_note = deliveryNote;
}
return originalAction();
});
};
});
// view/frontend/requirejs-config.js
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/set-shipping-information': {
'Vendor_Module/js/action/set-shipping-information-mixin': true
}
}
}
};
Step 2 – Add the HTML field to the shipping step
<!-- view/frontend/layout/checkout_index_index.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>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shipping-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="shippingAddress" xsi:type="array">
<item name="children" xsi:type="array">
<item name="before-form" xsi:type="array">
<item name="children" xsi:type="array">
<item name="delivery-note" xsi:type="array">
<item name="component" xsi:type="string">Magento_Ui/js/form/field</item>
<item name="config" xsi:type="array">
<item name="customScope" xsi:type="string">shippingAddress</item>
<item name="template" xsi:type="string">Vendor_Module/form/field</item>
<item name="elementTmpl" xsi:type="string">ui/form/element/textarea</item>
</item>
<item name="provider" xsi:type="string">checkoutProvider</item>
<item name="dataScope" xsi:type="string">shippingAddress.extension_attributes.delivery_note</item>
<item name="label" xsi:type="string" translate="true">Delivery note</item>
<item name="sortOrder" xsi:type="string">200</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Step 3 – Save delivery note to Quote and Order (PHP)
<!-- etc/extension_attributes.xml -->
<extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
<attribute code="delivery_note" type="string"/>
</extension_attributes>
<extension_attributes for="Magento\Sales\Api\Data\OrderInterface">
<attribute code="delivery_note" type="string"/>
</extension_attributes>
<?php
declare(strict_types=1);
namespace Vendor\Module\Plugin;
// Plugin on ShippingInformationManagement - intercepts the checkout "save shipping" call
class SaveDeliveryNotePlugin
{
public function __construct(
private \Magento\Quote\Api\CartRepositoryInterface $quoteRepository
) {}
public function beforeSaveAddressInformation(
\Magento\Checkout\Model\ShippingInformationManagement $subject,
int $cartId,
\Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation
): array {
$shippingAddress = $addressInformation->getShippingAddress();
$extensionAttrs = $shippingAddress->getExtensionAttributes();
$deliveryNote = $extensionAttrs?->getDeliveryNote() ?? '';
if (!empty($deliveryNote)) {
$quote = $this->quoteRepository->getActive($cartId);
$quote->setData('delivery_note', $deliveryNote);
$this->quoteRepository->save($quote);
}
return [$cartId, $addressInformation];
}
}
// Observer - copy delivery note from Quote to Order when order is placed
class CopyDeliveryNoteToOrderObserver implements \Magento\Framework\Event\ObserverInterface
{
public function execute(\Magento\Framework\Event\Observer $observer): void
{
$order = $observer->getData('order');
$quote = $observer->getData('quote');
$deliveryNote = $quote->getData('delivery_note');
if (!empty($deliveryNote)) {
$extensionAttrs = $order->getExtensionAttributes()
?? $this->orderExtensionFactory->create();
$extensionAttrs->setDeliveryNote($deliveryNote);
$order->setExtensionAttributes($extensionAttrs);
$order->setData('delivery_note', $deliveryNote); // also as direct field
}
}
}
Database schema – store delivery note on order
<?php
namespace Vendor\Module\Setup\Patch\Schema;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\Patch\SchemaPatchInterface;
class AddDeliveryNoteToOrder implements SchemaPatchInterface
{
public function __construct(
private \Magento\Framework\Setup\SchemaSetupInterface $schemaSetup
) {}
public function apply(): static
{
$this->schemaSetup->startSetup();
$this->schemaSetup->getConnection()->addColumn(
$this->schemaSetup->getTable('sales_order'),
'delivery_note',
[
'type' => Table::TYPE_TEXT,
'length' => 1000,
'nullable' => true,
'comment' => 'Customer delivery note',
]
);
$this->schemaSetup->endSetup();
return $this;
}
public static function getDependencies(): array { return []; }
public function getAliases(): array { return []; }
}
Summary
Checkout customisation in Magento 2 spans five layers: JS layout XML, JS mixin, RequireJS config, extension attributes XML, and PHP plugins or observers. The pattern is always the same: declare the data in extension_attributes.xml, intercept save on the PHP side via plugin, copy from Quote to Order via observer. The XML layout nesting is verbose but follows a consistent structure once you recognise it.
