Adding a Step

Registering a checkout step with the step navigator, placing it by sort order, and validating before the shopper moves on.

SyntaxstepNavigator.registerStep(code, alias, title, isVisible, navigate, sortOrder)

Checkout has two steps out of the box: shipping and review and payments. A third one is a component under checkout.steps that registers itself with the step navigator, the model at Magento_Checkout/js/model/step-navigator that keeps the list of steps, shows one at a time and drives the progress bar.

Registering

In initialize(), call stepNavigator.registerStep(code, alias, title, isVisible, navigate, sortOrder). code is a short identifier that also becomes the URL fragment, isVisible is a Knockout observable the navigator toggles, and navigate is the function it calls when the step becomes current. Pass null for alias unless the step answers to a second name.

Ordering and moving on

Order comes from sortOrder, declared on the node in checkout_index_index.xml and read by the component as this.sortOrder. Core uses 1 for shipping and 2 for billing in Magento 2.4.9, so a step after both needs a higher number. stepNavigator.next() hides the current step and shows the next by sort order, and writes the new fragment.

Validating first

Nothing validates for you. Check your own state before calling next() and return early if it fails, adding a message so the shopper knows why nothing happened.

What trips people up

JavaScript and templates are static assets: after editing, run bin/magento cache:flush, and bin/magento setup:static-content:deploy -f in production mode.

Example

Example · javascript
// app/code/Vendor/GiftStep/view/frontend/web/js/view/gift-step.js
define([
    'ko',
    'underscore',
    'uiComponent',
    'mage/translate',
    'Magento_Checkout/js/model/step-navigator'
], function (ko, _, Component, $t, stepNavigator) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_GiftStep/gift-step'
        },

        isVisible: ko.observable(false),

        initialize: function () {
            this._super();

            stepNavigator.registerStep(
                'gift',
                null,
                $t('Gift options'),
                this.isVisible,
                _.bind(this.navigate, this),
                this.sortOrder
            );

            return this;
        },

        navigate: function () {
            this.isVisible(true);
        },

        navigateToNextStep: function () {
            stepNavigator.next();
        }
    });
});

When to use it

  • Collect gift or delivery-slot details between shipping and payment.
  • Make shoppers accept store-specific terms before reaching payment.
  • Insert an age or licence check that must pass before an order can be placed.
  • Reorder an existing third-party checkout step by changing its sortOrder.

More examples

Placing the step in the tree

sortOrder 15 puts the step after shipping (1) and billing (2). Leave gaps between numbers so a later module can slot in without renumbering yours. The node sits directly under steps, whose default container template renders each child, so no displayArea is needed here. Run bin/magento cache:flush and reload; the step appears in the progress bar with the title passed to registerStep, and its URL fragment is #gift.

Example · xml
<!-- app/code/Vendor/GiftStep/view/frontend/layout/checkout_index_index.xml -->
<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="gift-step" xsi:type="array">
                                            <item name="component" xsi:type="string">Vendor_GiftStep/js/view/gift-step</item>
                                            <item name="sortOrder" xsi:type="string">15</item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

The step template

The outer element is an <li> with visible bound to the observable passed to registerStep, because steps render inside the checkout's ordered list and the navigator shows one at a time. Keeping the step-title, step-content, data-role and class names that core steps use means the stock Luma styles apply without new LESS. The click binding names the method without parentheses; Knockout calls it with the view model as context.

Example · html
<!-- app/code/Vendor/GiftStep/view/frontend/web/template/gift-step.html -->
<li id="gift-step" class="checkout-gift" visible="isVisible()">
    <div class="step-title" translate="'Gift options'" data-role="title"></div>

    <div id="checkout-step-gift" class="step-content" data-role="content">
        <div class="field choice">
            <input type="checkbox" id="gift-wrap" ko-checked="isWrapped"/>
            <label for="gift-wrap"><span translate="'Wrap this order'"></span></label>
        </div>

        <div class="field">
            <label for="gift-message"><span translate="'Message'"></span></label>
            <textarea id="gift-message" rows="3" ko-value="message"></textarea>
        </div>

        <div class="actions-toolbar">
            <button type="button" class="button action continue primary" click="navigateToNextStep">
                <span translate="'Next'"></span>
            </button>
        </div>
    </div>
</li>

Validating before moving on

The navigator does not know or care whether your step is complete, so the guard belongs in the click handler. messageList is the model behind the checkout message area, so an error pushed to it appears where shoppers already look for problems. Returning early leaves the step visible with its values intact. Gift wrapping as a configurable product option is an Adobe Commerce feature; a step like this is how you collect the same information on Open Source.

Example · javascript
// app/code/Vendor/GiftStep/view/frontend/web/js/view/gift-step.js (excerpt)
define([
    'ko',
    'mage/translate',
    'Magento_Ui/js/model/messageList',
    'Magento_Checkout/js/model/step-navigator'
], function (ko, $t, messageList, stepNavigator) {
    'use strict';

    return {
        isWrapped: ko.observable(false),
        message: ko.observable(''),

        validate: function () {
            if (this.isWrapped() && this.message().trim() === '') {
                messageList.addErrorMessage({
                    message: $t('Enter a gift message or turn off gift wrapping.')
                });

                return false;
            }

            return true;
        },

        navigateToNextStep: function () {
            if (!this.validate()) {
                return;
            }

            stepNavigator.next();
        }
    };
});

Discussion

  • Be the first to comment on this lesson.