When you are running an e-commerce store, inevitably you will experience shopping cart abandonment - customer adds items to their cart but leaves your site before checking out. According to Baymard Institute information, the average shopping cart abandonment rate is almost 70%. There are a number of reasons for this phenomenon and one of them is the complicated checkout process.

This article will help to make the checkout process of your Magento 2 store more user-friendly. We will describe how to minimize confusion when dealing with validation errors in checkout forms.
Let’s start!

The problem

If the user submits a form with invalid or missing information e.g. incorrect phone number, if the field is not in the current view it's not immediately clear what the error is. The user is often confused as to why they can't proceed to the next step of the checkout process until they scroll up to see the error. It would make much more sense if the user was taken to the error message automatically as soon as the message popped up on the screen.

Client-side validation messages

Here's how to implement a solution to the above issue. First, we’ll implement auto-scroll to the first invalid field for shipping address form on checkout:

1) Create shipping-mixin.js file in app\design\frontend\\\Magento_Checkout\web\js\view:

define(
   [
       'jquery',
       'underscore',
       'Magento_Ui/js/form/form',
       'ko',
       'Magento_Customer/js/model/customer',
       'Magento_Customer/js/model/address-list',
       'Magento_Checkout/js/model/address-converter',
       'Magento_Checkout/js/model/quote',
       'Magento_Checkout/js/action/create-shipping-address',
       'Magento_Checkout/js/action/select-shipping-address',
       'Magento_Checkout/js/model/shipping-rates-validator',
       'Magento_Checkout/js/model/shipping-address/form-popup-state',
       'Magento_Checkout/js/model/shipping-service',
       'Magento_Checkout/js/action/select-shipping-method',
       'Magento_Checkout/js/model/shipping-rate-registry',
       'Magento_Checkout/js/action/set-shipping-information',
       'Magento_Checkout/js/model/step-navigator',
       'Magento_Ui/js/modal/modal',
       'Magento_Checkout/js/model/checkout-data-resolver',
       'Magento_Checkout/js/checkout-data',
       'uiRegistry',
       'mage/translate',
       'Magento_Checkout/js/model/shipping-rate-service'
   ],function (
       $,
       _,
       Component,
       ko,
       customer,
       addressList,
       addressConverter,
       quote,
       createShippingAddress,
       selectShippingAddress,
       shippingRatesValidator,
       formPopUpState,
       shippingService,
       selectShippingMethodAction,
       rateRegistry,
       setShippingInformationAction,
       stepNavigator,
       modal,
       checkoutDataResolver,
       checkoutData,
       registry,
       $t) {
       'use strict';

       var mixin = {
       };

       return function (target) {
           return target.extend(mixin);
       };
   });


2) Include this mixin in app\design\frontend\\\Magento_Checkout\requirejs-config.js

var config = {
  config: {
       mixins: {
           ‘Magento_Checkout/js/view/shipping': {
               'Magento_Checkout/js/view/shipping-mixin': true
           }
       }
   }
};


3) In ‘mixin’ variable from the shipping-mixin.js file we'll do the following:

  • define a function, which will scroll the page to invalid field,
  • add an invocation of this function when the form validation return errors.
var mixin = {
    /**
* ScrollTo invalid input field
* @param errorElement
* @param scrollElement
*/
    onErrorFocus: function(errorElement, scrollElement) {
        if (typeof errorElement !== 'undefined') {
           if (!($(errorElement).is(':visible'))) {
               $('.google-fill-address').trigger('click');
           }
           if (typeof scrollElement == 'undefined') {
               var scrollElement = $('html, body'),
                   windowHeight = $(window).height(),
                   errorElement_offset = $(errorElement).offset().top,
                   scroll_top = errorElement_offset - windowHeight / 3;
           } else {
               var modalElement = scrollElement.find('.modal-content'),
                   modal_offset = modalElement.offset().top,
                   errorElement_offset = $(errorElement).offset().top,
                   scroll_top;
               scrollElement = modalElement;
               if (modal_offset < errorElement_offset) {
                   scroll_top = errorElement_offset - modal_offset;
               } else {
                   scroll_top = errorElement_offset + modal_offset;
               }
           }
           scrollElement.animate({
               scrollTop: scroll_top
           });
        }
    },

    setShippingInformation: function () {
        if (!this.validateShippingInformation()) {
           var errorElement = 
           $("div[class*='mage-error']:visible, div[class*='field-error']:visible")[0];
           this.onErrorFocus(errorElement);
       }
       this._super();
    },

    saveNewAddress: function () {
       this._super();
       if (this.source.get('params.invalid')) {
           var errorElement = 
           $("div[class*='mage-error']:visible, div[class*='field-error']:visible")[0],
           scrollElement = $(this.getPopUp().element).parents('.modal-popup');
           this.onErrorFocus(errorElement, scrollElement);

       }
    }
};

NOTE 1: When we get the first error (errorElement) we are using 2 classnames:
mage-error - is used in Magento 2.x versions
field-error - is used in Magento_Ui knockout template for input field in Magento 2.2 version

NOTE 2: We are extending 2 default functions from shipping.js file:
setShippingInformation is used in shipping form on Checkout for unregistered users
setNewAddress is used on Shipping step on Checkout for logged in users, in New/Edit Address popup

Similarly, you can implement the same behavior in the second step of checkout.

Errors received from the server after performing Ajax requests

Once user passes client-side validation there's still server-side validation that needs to be performed before completing an order. If something went wrong on the server while placing order, Magento displays error messages from server at the top of the page for 5 seconds before dismissing them.

Again, if the "Place order" button is below the fold, then the customer might think nothing has happened and try to place order again without realizing his request has already been rejected.

Errors block default position when Place Order

One solution to this issue would be to display .checkout_messages block with errors and notices directly above the ‘Place Order’ button. It can be done easily with CSS. In app\design\frontend\\\web\css\source_extend.less

& when (@media-common = true) {
   .checkout-payment-method {
       .payment-method {
           &._active {
               .payment-method-content {
                   .lib-vendor-prefix-display(flex);
                   .lib-vendor-prefix-flex-direction(column);

                   .messages {
                       .lib-vendor-prefix-order(2);
                   }

                   .actions-toolbar {
                       .lib-vendor-prefix-order(3);
                   }
               }
           }
       }
   }
}


As a result we have the following:

Errors block improved position when Place Order

Also, you can increase the timeout for hiding message.
For this, we should extend function onHiddenChange from Magento_Ui/js/view/messages script.
As in previous part, we can do it by using Magento’s javascript mixin:
1) Create a mixin file and set time = 10000 (by default 5000) for setTimeout function app\design\frontend\\\Magento_Checkout\web\js\view\messages-mixin.js

define(['jquery'], function ($) {
   'use strict';

   var mixin = {
       onHiddenChange: function (isHidden) {
           var self = this;
           // Hide message block if needed
           if (isHidden) {
               setTimeout(function () {
                   $(self.selector).slideUp(500)
               }, 10000);
           }
       }
   };

   return function (target) { 
       return target.extend(mixin);
   };
});


2) add the mixin into requirejs-config.js file:

var config = {
   config: {
       mixins: {
           'Magento_Ui/js/view/messages': {
               'Magento_Checkout/js/view/messages-mixin': true
           }
       }
   }
};

Conclusion:

These little improvements will help make the process of the checkout more transparent to the customers and reduce the percentage of abandoned shopping carts in your Magento 2 store.