By default, Magento 2 reloads the page (or redirects to the cart) every time a customer clicks Add to Cart. That interruption costs conversions — the customer loses their place in the catalog and has to navigate back to keep shopping. In this guide, you'll add products to the cart using Ajax programmatically: the product is added in the background, the mini cart updates instantly, and a confirmation popup appears — all without a page refresh. This is the same core behavior our Magento 2 Ajax Add to Cart extension provides out of the box, with configurable popups, related products, and countdown timers on top.
How Ajax Add to Cart Works in Magento 2
Magento 2 already ships with an Ajax-capable add-to-cart widget: Magento_Catalog/js/catalog-add-to-cart. On product pages, the form submit is intercepted, sent via $.ajax as a POST request, and the response updates the mini cart and messages. What the default behavior lacks is any visual confirmation beyond the small success message — so customers often don't notice the product was added. The customization below hooks into that widget with a RequireJS mixin and shows a modal popup with Continue and Checkout buttons after every successful add.
Set Up the Custom Module
The mixin needs to live inside a module. We'll use Mageants_Blog as the example namespace — replace it with your own vendor and module name. Create these two files first:
Create app/code/Mageants/Blog/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Mageants_Blog',
__DIR__
);
Create app/code/Mageants/Blog/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Mageants_Blog"/>
</config>
Show a Popup After Add to Cart (Magento 2 Add to Cart Popup)
Step 1: Declare the RequireJS Mixin
Create app/code/Mageants/Blog/view/frontend/requirejs-config.js. This tells RequireJS to load our file on top of Magento's core add-to-cart widget wherever it's used:
var config = {
config: {
mixins: {
'Magento_Catalog/js/catalog-add-to-cart': {
'Mageants_Blog/js/catalog-add-to-cart': true
}
}
}
};
Step 2: Override ajaxSubmit and Add the Modal
Create app/code/Mageants/Blog/view/frontend/web/js/catalog-add-to-cart.js. The widget below re-declares ajaxSubmit: it posts the form data, updates the mini cart and messages from the JSON response, and — this is our addition — opens a modal popup with Continue and Checkout buttons in the success callback:
define([
'jquery',
'mage/translate',
'underscore',
'Magento_Catalog/js/product/view/product-ids-resolver',
'Magento_Catalog/js/product/view/product-info-resolver',
'Magento_Ui/js/modal/modal',
'jquery-ui-modules/widget'
], function ($, $t, _, idsResolver, productInfoResolver, modal) {
'use strict';
return function (widget) {
$.widget('mage.catalogAddToCart', widget, {
options: {
processStart: null,
processStop: null,
bindSubmit: true,
minicartSelector: '[data-block="minicart"]',
messagesSelector: '[data-placeholder="messages"]',
productStatusSelector: '.stock.available',
addToCartButtonSelector: '.action.tocart',
addToCartButtonDisabledClass: 'disabled',
addToCartButtonTextWhileAdding: '',
addToCartButtonTextAdded: '',
addToCartButtonTextDefault: '',
productInfoResolver: productInfoResolver
},
/**
* Handle form submission via Ajax and show a popup on success.
* @param {jQuery} form
*/
ajaxSubmit: function (form) {
var self = this,
productIds = idsResolver(form),
productInfo = self.options.productInfoResolver(form),
formData;
$(self.options.minicartSelector).trigger('contentLoading');
self.disableAddToCartButton(form);
formData = new FormData(form[0]);
$.ajax({
url: form.attr('action'),
data: formData,
type: 'post',
dataType: 'json',
cache: false,
contentType: false,
processData: false,
/** @inheritdoc */
beforeSend: function () {
if (self.isLoaderEnabled()) {
$('body').trigger(self.options.processStart);
}
},
/** @inheritdoc */
success: function (res) {
var eventData, parameters, popup;
$(document).trigger('ajax:addToCart', {
'sku': form.data().productSku,
'productIds': productIds,
'productInfo': productInfo,
'form': form,
'response': res
});
if (self.isLoaderEnabled()) {
$('body').trigger(self.options.processStop);
}
if (res.backUrl) {
eventData = {
'form': form,
'redirectParameters': []
};
// Let other modules add parameters to the redirect URL
$('body').trigger('catalogCategoryAddToCartRedirect', eventData);
if (eventData.redirectParameters.length > 0 &&
window.location.href.split(/[?#]/)[0] === res.backUrl
) {
parameters = res.backUrl.split('#');
parameters.push(eventData.redirectParameters.join('&'));
res.backUrl = parameters.join('#');
}
self._redirect(res.backUrl);
return;
}
if (res.messages) {
$(self.options.messagesSelector).html(res.messages);
}
if (res.minicart) {
$(self.options.minicartSelector).replaceWith(res.minicart);
$(self.options.minicartSelector).trigger('contentUpdated');
}
if (res.product && res.product.statusText) {
$(self.options.productStatusSelector)
.removeClass('available')
.addClass('unavailable')
.find('span')
.html(res.product.statusText);
}
self.enableAddToCartButton(form);
// Confirmation popup with Continue / Checkout actions
popup = $('<div class="add-to-cart-modal-popup"/>')
.html($('.page-title span').text() +
'<span> has been added to cart.</span>')
.modal({
modalClass: 'add-to-cart-popup',
title: $t('Added to Cart'),
buttons: [
{
text: $t('Continue Shopping'),
click: function () {
this.closeModal();
}
},
{
text: $t('Go to Checkout'),
click: function () {
window.location = window.checkout.checkoutUrl;
}
}
]
});
popup.modal('openModal');
},
/** @inheritdoc */
error: function (res) {
$(document).trigger('ajax:addToCart:error', {
'sku': form.data().productSku,
'productIds': productIds,
'productInfo': productInfo,
'form': form,
'response': res
});
},
/** @inheritdoc */
complete: function (res) {
if (res.state() === 'rejected') {
location.reload();
}
}
});
}
});
return $.mage.catalogAddToCart;
};
});
Step 3: Enable the Module and Deploy
Run these commands from the Magento root, then hard-refresh the storefront (the mixin is loaded by RequireJS, so stale static content is the most common reason it "doesn't work"):
php bin/magento module:enable Mageants_Blog php bin/magento setup:upgrade php bin/magento setup:static-content:deploy -f php bin/magento cache:flush
Add any product to the cart from a product page and the popup appears with both action buttons:
Update Cart Quantity with Ajax
Quantity updates in the mini cart are already Ajax-powered in default Magento 2 — no code needed. To verify it on your store: add any product to the cart, open the mini cart, and change the value in the Qty field. The subtotal and item count update immediately without a page refresh.
Enable or Disable Ajax Add to Cart from the Admin
Whether the storefront uses Ajax or redirects to the cart page is controlled by a single native setting. The naming trips people up, so to be clear: the setting is about the redirect, and Ajax behaves as the opposite of it.
Step 1
On the admin sidebar, go to Stores > Settings > Configuration.
Step 2
In the left panel, expand Sales and choose Checkout.
Step 3
Expand the Shopping Cart section.
Step 4
If the setting should apply to a specific store view only, switch the Store View scope at the top left and click OK when prompted.
Step 5
Find After Adding a Product Redirect to Shopping Cart and choose:
No — the customer stays on the current page and the product is added via Ajax. This is the setting you want for the popup customization above.
Yes — the customer is redirected to the cart page after every add, which effectively disables the Ajax behavior. Some stores with single-product checkout flows prefer this to push customers toward checkout faster.
Step 6
Click Save Config and flush the cache.
Enable Ajax for Add-to-Cart Buttons in Product Widgets
Product widgets on the home page and CMS pages ship with Ajax turned off — their add-to-cart forms have bindSubmit set to false, so clicking the button does a full-page submit. To fix that, override the template in your theme:
app/design/frontend/VendorName/themename/Magento_Catalog/templates/product/view/addtocart.phtml
Find the x-magento-init block and change "bindSubmit": false to "bindSubmit": true:
<script type="text/x-magento-init">
{
"#product_addtocart_form": {
"catalogAddToCart": {
"bindSubmit": false
}
}
}
</script>
becomes:
<script type="text/x-magento-init">
{
"#product_addtocart_form": {
"catalogAddToCart": {
"bindSubmit": true
}
}
}
</script>
One prerequisite: this only works when Stores > Configuration > Sales > Checkout > Shopping Cart > After Adding a Product Redirect to Shopping Cart is set to No — otherwise the redirect wins and the Ajax submit never fires.
Conclusion
That's the complete picture: the native widget handles the Ajax request, a RequireJS mixin adds the confirmation popup, one admin setting toggles the behavior store-wide, and a template override extends it to home page widgets. If you'd rather not maintain custom code — and want configurable popups, related-product upsells, and countdown timers without touching a mixin — our Ajax Add to Cart extension covers all of it from the admin panel.
And if the reason you're adding products to the cart programmatically is promotional — gifting an item automatically when the cart hits a threshold or a specific product is purchased — you don't need this customization at all. A Magento 2 extension to auto-add free products to cart handles the entire flow with admin-configurable promotion rules, including the Ajax add itself.
Ran into an issue with the implementation, or need something more custom? Our Magento development team can help — or drop your question in the comments below.