Upsell with add ons on the product page

There are products where you need to showcase an optional accessory without pulling the customer away from the current product page. Take a quick look at the function of the simply titled product "Product with Add on".

Product with add on product page

For this example, we are keeping it simple with a product with no variants with an option to add another product. The upsell product also does not have variants. This works with the customer clicking the checkbox, choosing their quantity, and adding to cart. It is also not visible at first and only shown if the product is available and not sold out. Test it out and you should get both products in your cart. 

Here is the snippet for the creating the checkbox, title, price, and quantity input.

<div id="product-add-on" class="product-add-on row mx-0 d-none">
  <div class="d-inline-block py-2 mr-auto col-auto px-0">
    <input type="checkbox" id="product-add-on-checkbox" name="product-add-on-checkbox" value="product-add-on-checkbox">
    <label for="product-add-on"> <span id="product-add-on-name"></span> </label> <label for="product-add-on-qty"><strong id="product-add-on-price"></strong></label>
  </div>
  <div class="product-options-bottom border-0 mt-0 d-inline-block col-auto px-0">
    <div class="add-to-cart-box">
      <div class="input-box">
        <input type="text" id="product-add-on-qty" name="product-add-on-quantity" value="1" min="1" class="quantity-selector">
        <div class="plus-minus">
          <div class="increase items" onclick="var result = document.getElementById('product-add-on-qty'); var qty = result.value; if( !isNaN( qty )) result.value++;return false;">
            <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-plus" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
            </svg>
          </div>
          <div class="reduced items" onclick="var result = document.getElementById('product-add-on-qty'); var qty = result.value; if( !isNaN( qty ) && qty > 1 ) result.value--;return false;">
            <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-dash" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
            </svg>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

The title and price are dynamic and pull from the product so there is one place of truth for those variables and they are added in the span tags with id="product-add-on-name" and id="product-add-on-price" respectively. Plus and minus buttons use inline click functions to handle the quantity input. 

This is all brought together with adding this script:

$(document).ready(function () {
      var productAddOnHandle = "add-on-product"
      var productAddOnId = 0;
      var productAddOnQty = 1;
      var productAddOnAvailable = false;
      jQuery.getJSON('/products/' + productAddOnHandle + '.js', function (product) {
        productAddOnAvailable = product.variants[0].available;
        if (productAddOnAvailable) {
          productAddOnId = product.variants[0].id;
          $("#product-add-on").removeClass("d-none");
          $("#product-add-on-name").html(product.title);
          $("#product-add-on-price").html("+ $" + (product.price_min / 100).toFixed(2));
        }
      });
      $('#add-to-cart-button').click(function (e) {
        e.preventDefault();
        var productAddOnChecked = $("#product-add-on-checkbox").is(":checked");
        if (productAddOnChecked && productAddOnId !== 0 && productAddOnAvailable) {
          productAddOnQty = $('#product-add-on-qty').val();
          productAddToCart();
        } else {
          $("form[action='/cart/add']").submit();
        }
      });
      function productAddToCart() {
        $.ajax({
          url: "/cart/add.js",
          type: "POST",
          data: {
            items: [
              {
                quantity: productAddOnQty,
                id: productAddOnId
              }
            ]
          },
          dataType: "json"
        })
          .done(function () {

          })
          .always(function () {
            $("form[action='/cart/add']").submit();
          });
      }
    });

The script uses Shopify AJAX Product API to get json data. This queries the product based on the product handle, checks if it's available, adds visibility to the option if it is, finally returns the product id, title, and formatted price. A click function is added to the add to cart button, while preventing the submit form function using e.preventDefault();. I also use a button tag instead of an input to handle form submission. I don't add the type="submit" as an extra measure if the page uses the product add on.

<button id="add-to-cart-button" class="btn btn-prime w-100 text-white" {% if product.handle != "product-with-add-on" %} type="submit" {% endif %}>Add to cart</button>

To submit the form for adding to cart we use this script:

$("form[action='/cart/add']").submit();

Finally, once the quantity is retrieved and the checkbox is checked we add the product add on to the cart before submitting the form which adds the main product. This is done by using Shopify AJAX Cart API, where we only need to post data to /cart/add.js using an array of items with quantity and product id. I'm submitting on the "always" callback function to insure that the customer moves to the cart page whether or not the script works to prevent a loss of sale.

function productAddToCart() {
        $.ajax({
          url: "/cart/add.js",
          type: "POST",
          data: {
            items: [
              {
                quantity: productAddOnQty,
                id: productAddOnId
              }
            ]
          },
          dataType: "json"
        })
          .done(function () {

          })
          .always(function () {
            $("form[action='/cart/add']").submit();
          });
      }

I believe that this one function will help by not adding the complexity of adding another monthly charged plugin to your store, especially if you only need an add on for one product.

There is more to be imagined for this tutorial. We can make a metafield for products with an array of product handles so you can add as multiple add ons. If the metafield is not empty, add the elements shown on this tutorial. Then you would check each product and add the checkbox if the product is available, but you will have to create an items array variable to push checked products to be added to the cart. 

Also, adding another complexity of a product add on that has variants attached to it. I imagine a t-shirt add on that has the variants with inlined options and dropdowns. It would be as follows: checkbox, "Add discounted t-shirt", "Size: [dropdown]", "Color: [dropdown]", Price. The difficult part is getting the correct product variant id and adding it to the cart. 

Leave a comment below this tutorial helps you improve your store. Maybe you need more insight of this code or I didn't fully explain part of the snippet. Any feedback is welcomed. Happy coding!

Joe Pichardo | Shopify Developer

About Joe Pichardo

Joe Pichardo is a Shopify Developer creating themes and apps to help other programmers succeed on the ecommerce platform.

Having trouble? Ask for help.