Skip to content
Cart Drawer Upsell Recommendations - Free Tutorial
Browse other ways to boost conversion rate & profit

Cart Drawer Upsell Recommendations - Free Tutorial

In this tutorial, we're showing you how to easily add auto-generated in-cart product upsells to your Shopify store without any apps or knowing how to code. Customers can add these recommended products to their cart without leaving the page, making it a highly effective way to increase your AOV.

Compatible Themes: This code should work on all free Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight).

 

Add cart recommendations settings to settings_schema.json

  {
    "name": "Cart Drawer Recommendations Upsell",
    "settings": [
      {
        "type": "checkbox",
        "id": "enable_cart_upsell",
        "label": "Enable Cart Drawer Recommendations",
        "default": false
      },
      {
        "type": "header",
        "content": "Formatting Settings"
      },
      {
        "type": "color",
        "id": "cart_drawer_recommendations_background_color",
        "label": "Background Color",
        "default": "#f2f2f2"
      },
      {
        "type": "text",
        "id": "cart_drawer_recommendations_heading",
        "label": "Heading Text",
        "default": "You may also like"
      },
      {
        "type": "color",
        "id": "cart_drawer_recommendations_heading_color",
        "label": "Heading Color",
        "default": "#333333"
      },
      {
        "type": "range",  
        "id": "cart_drawer_recommendations_heading_font_size",
        "label": "Heading Font Size",
        "min": 16,
        "max": 28,
        "step": 1,
        "default": 20
      },
      {
        "type": "select",
        "id": "cart_drawer_recommendations_heading_font_weight",
        "label": "Heading Font Weight",
        "default": "bold",
        "options": [
          {
            "value": "normal",
            "label": "Normal"
          },
          {
            "value": "bold",
            "label": "Bold"
          },
          {
            "value": "bolder",
            "label": "Bolder"
          }
        ]
      },
      {
        "type": "range",
        "id": "cart_drawer_recommendation_media_width",
        "label": "Media Width",
        "min": 30,
        "max": 60,
        "step": 5,
        "default": 45
      },
      {
        "type": "checkbox",
        "id": "cart_drawer_recommendation_hide_variant_titles",
        "label": "Hide Variant Titles",
        "default": true
      },
      {
        "type": "select",
        "id": "cart_drawer_recommendation_picker_type",
        "options": [
          {
            "value": "dropdown",
            "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.options__1.label"
          },
          {
            "value": "button",
            "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.options__2.label"
          }
        ],
        "default": "dropdown",
        "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.label"
      },
      {
        "id": "cart_drawer_recommendation_swatch_shape",
        "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.label",
        "type": "select",
        "info": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.info",
        "options": [
          {
            "value": "circle",
            "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__1.label"
          },
          {
            "value": "square",
            "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__2.label"
          },
          {
            "value": "none",
            "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__3.label"
          }
        ],
        "default": "none"
      },
      {
        "type": "color",
        "id": "cart_drawer_recommendation_btn_color",
        "label": "Add To Cart Button Color",
        "default": "#000000"
      },
      {
        "type": "color",
        "id": "cart_drawer_recommendation_btn_text_color",
        "label": "Add To Cart Button Text Color",
        "default": "#ffffff"
      },
      {
        "type": "header",
        "content": "Upsell Product Selection Settings"
      },
      {
        "type": "select",
        "id": "cart_drawer_upsell_type",
        "label": "Cart Upsell Type",
        "options": [
          {
            "value": "recommendations",
            "label": "Auto Recommendations"
          },
          {
            "value": "complementary",
            "label": "Complementary"
          }
        ],
        "default": "recommendations",
        "info": "Auto Recommendations are automatically generated based on cart contents. Complementary is based on selected complementary products in the Shopify Search & Discovery app."
      },
      {
        "type": "range",
        "id": "cart_drawer_recommendations_products_to_show",
        "label": "Upsell Products to Show",
        "min": 1,
        "max": 5,
        "step": 1,
        "default": 3
      },
      {
        "type": "header",
        "content": "Recommendations Settings"
      },
      {
        "type": "range",
        "id": "cart_drawer_recommendations_products_to_recommend",
        "label": "Products to Recommend per Cart Item",
        "min": 1,
        "max": 10,
        "step": 1,
        "default": 4,
        "info": "For Auto Recommendations calculations only. Adjust this number to increase the pool of potential products to recommend in the final ranking. Must be equal or greater than the setting Upsell Products To Show."
      },
      {
        "type": "range",
        "id": "recommendation_weight_quantity",
        "label": "Recommendation Weight Factor: Quantity",
        "min": 0,
        "max": 1,
        "step": 0.1,
        "default": 0.3
      },
      {
        "type": "range",
        "id": "recommendation_weight_frequency",
        "label": "Recommendation Weight Factor: Frequency",
        "min": 0,
        "max": 1,
        "step": 0.1,
        "default": 0.4
      },
      {
        "type": "range",
        "id": "recommendation_weight_position",
        "label": "Recommendation Weight Factor: Position",
        "min": 0,
        "max": 1,
        "step": 0.1,
        "default": 0.3
      }
    ]
  }

 

Add cart recommendations snippet to cart-drawer.liquid

        {% if settings.enable_cart_upsell and cart.items.size > 0 %}
          {% render 'cart-drawer-recommendations' %}
        {% endif %}

Create new liquid snippet cart-drawer-recommendations.liquid

{% comment %}
  Generates and renders cart drawer recommendations
{% endcomment %}

<div class="cart-drawer__recommendations">

  {% assign products_to_recommend = settings.cart_drawer_recommendations_products_to_recommend %}
  {% assign products_to_show = settings.cart_drawer_recommendations_products_to_show %}
  {% assign section_id = "cart-drawer" %}
  {% capture cart_items_json %}
    [
      {% for item in cart.items %}
        {
          "product_id": {{ item.product_id | json }},
          "quantity": {{ item.quantity | json }}
        }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ]
  {% endcapture %}
 
  <div class="cart-drawer__recommendations-heading">{{ settings.cart_drawer_recommendations_heading }}</div>
  <comprehensive-cart-recommendations
    class="complementary-products"
    data-url="{{ routes.product_recommendations_url }}?limit={{ products_to_recommend }}{% if settings.cart_drawer_upsell_type == 'complementary' %}&intent=complementary{% endif %}"
    data-section-id="{{ section_id }}"
    data-cart-items="{{ cart_items_json | strip_newlines | strip | escape }}"
    data-max-position="{{ products_to_recommend }}"
    data-products-to-show="{{ products_to_show }}"
    data-weight-quantity="{{ settings.recommendation_weight_quantity }}"
    data-weight-frequency="{{ settings.recommendation_weight_frequency }}"
    data-weight-position="{{ settings.recommendation_weight_position }}"
    >
      {% if recommendations.performed and recommendations.products_count > 0 %}
        <ul class="grid product-grid">
          {% for recommendation in recommendations.products %}
            <li class="grid__item">
              {% render 'cart-drawer-upsell-product', product: recommendation, section_id: section_id %}
            </li>
          {% endfor %}
        </ul>
      {% endif %}
  </comprehensive-cart-recommendations>
</div>

<style>
  .cart-drawer__recommendations {
    margin-top: 20px;
    border-top: 1px solid var(--color-border);
    background-color: {{ settings.cart_drawer_recommendations_background_color }};
    padding: 10px;
    display: none;
  }

  .cart-drawer__recommendations.has-recommendations {
    display: block; 
  }

  .cart-drawer__recommendations .complementary-products .grid__item {
    width: 100%;
    max-width: 100%;
    margin-bottom: 10px;
  }

  .cart-drawer__recommendations-heading {
    font-size: {{ settings.cart_drawer_recommendations_heading_font_size }}px;
    font-weight: {{ settings.cart_drawer_recommendations_heading_font_weight }};
    color: {{ settings.cart_drawer_recommendations_heading_color }};
    margin-bottom: 20px;
    margin-top: 0;
    text-align: center;
  }

  .product-recommendations.no-markers {
    list-style-type: none;
    padding-left: 0;
    margin: 0;
  }

  .product-recommendations.no-markers li {
    list-style-type: none;
  }

  .product-recommendations.no-markers li::marker {
    display: none;
  }

  .cart-upsell-item {
    margin-bottom: 20px;
  }

  .cart-upsell-item__inner {
    display: flex;
    align-items: flex-start;
    gap: 15px;
    padding: 15px;
    border-color: rgba(var(--color-foreground), 0.75);
    background: var(--gradient-background);
  }

  .cart-upsell-item__media-wrapper {
    flex: 0 0 {{ settings.cart_drawer_recommendation_media_width }}%;
  }

  .cart-upsell-item__media img {
    width: 100%;
    height: auto;
    display: block;
    border-radius: 4px;
  }

  .cart-upsell-item__content {
    flex: 1;
    min-width: 0;
  }

  .cart-upsell-item__title {
    font-size: 16px;
    font-weight: 600;
    margin: 0 0 10px;
    line-height: 1.2;
  }

  .cart-upsell-item__link {
    color: inherit;
    text-decoration: none;
  }

  .cart-upsell-item__link:hover {
    text-decoration: underline;
  }

  .cart-upsell-item__price {
    font-size: 14px;
    margin-bottom: 10px;
  }

  .cart-upsell-item__variants {
    margin-bottom: 15px;
  }

  .cart-upsell-item__variants select {
    width: 100%;
    padding: 4px 8px;
    border-color: rgba(var(--color-foreground), 0.75);
    font-size: 14px;
  }

  .cart-upsell-item__variants .product-form__input--dropdown {
    margin-bottom: 0.7rem;
  }

  {% if settings.cart_drawer_recommendation_hide_variant_titles %}
    .cart-upsell-item__variants .product-form__input .form__label {
      display: none;
    }
  {% endif %}

  .cart-upsell-item__variants .product-form__input .select__select {
    height: 3rem;
  }

  .cart-upsell-item__variants .product-form__input--pill input[type=radio]+label {
    margin: .5rem .5rem .3rem 0;
    padding: 0.5rem 1rem;
    font-size: 1.25rem;
    letter-spacing: 0.06rem;
  }

  .cart-upsell-item__variants .product-form__input--swatch .swatch-input__input+.swatch-input__label {
    --swatch-input--size: 2.3rem;
    margin: .5rem 1.0rem .2rem 0;
  }

  .cart-upsell-item__button .product-form__submit {
    width: 100%;
    font-size: 14px;
    padding: 10px 15px;
    border-radius: 4px;
    border: none;
    background-color: {{ settings.cart_drawer_recommendation_btn_color }};
    color: {{ settings.cart_drawer_recommendation_btn_text_color }};
    cursor: pointer;
    transition: background-color 0.3s ease;
  }
    
  .cart-upsell-item__button .button:after {
        box-shadow: 0 0 0 calc(var(--buttons-border-width) + var(--border-offset))
        rgba(var(--color-button-text), var(--border-opacity)),
      0 0 0 var(--buttons-border-width) {{ settings.cart_drawer_recommendation_btn_color }};
  }

  .cart-upsell-item__button .button:not([disabled]):hover::after {
    box-shadow: 0 0 0 calc(var(--buttons-border-width) + var(--border-offset))
        rgba(var(--color-button-text), var(--border-opacity)),
      0 0 0 calc(var(--buttons-border-width) + 1px) {{ settings.cart_drawer_recommendation_btn_color }};
  }
  
</style>

{{ 'section-main-product.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
{{ 'component-product-variant-picker.css' | asset_url | stylesheet_tag }} 

 

Create new liquid snippet cart-drawer-upsell-product.liquid

{% comment %}
  Renders a simplified featured product for use in cart upsells
  Accepts:
  - product: {Object} Product object
  - section_id: {String} The ID of the section containing this product
{% endcomment %}

{%- assign product_form_id = 'product-form-' | append: section_id | append: '-' | append: product.id -%}

<product-info-cart-upsell
  id="ProductInfo-{{ section_id }}-{{ product.id }}"
  data-section="{{ section_id }}"
  data-url="{{ product.url }}"
  data-product-id="{{ product.id }}"
  data-product-handle="{{ product.handle }}"
  data-is-upsell
  class="cart-upsell-item"
  data-product-info
>
  <div class="cart-upsell-item__inner cart-upsell-product-container">
    <div class="cart-upsell-item__media-wrapper">
        {% assign media = product.selected_or_first_available_variant.featured_media | default: product.featured_media %}
        {% if media %}
          <div class="cart-upsell-item__media">
            <img
              srcset="{%- if media.width >= 165 -%}{{ media | image_url: width: 165 }} 165w,{%- endif -%}
                {%- if media.width >= 360 -%}{{ media | image_url: width: 360 }} 360w,{%- endif -%}
                {{ media | image_url }} {{ media.width }}w"
              src="{{ media | image_url: width: 165 }}"
              sizes="(min-width: 990px) 165px, 360px"
              alt="{{ media.alt | escape }}"
              loading="lazy"
              width="{{ media.width }}"
              height="{{ media.height }}"
              id="ProductImage-{{ section_id }}-{{ product.id }}"
              data-product-image
            >
          </div>
        {% else %}
          {{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
        {% endif %}
      </div>
    <div class="cart-upsell-item__content">
      <div class="cart-upsell-item__info-wrapper">
        <h3 class="cart-upsell-item__title">
          <a href="{{ product.url }}" class="cart-upsell-item__link">
            {{ product.title | escape }}
          </a>
        </h3>
        <div id="price-{{ section_id }}-{{ product.id }}" role="status" class="cart-upsell-item__price" data-price-wrapper>
          {%- render 'price',
            product: product,
            use_variant: true,
            show_badges: false,
            disable_cart_upsell_currency_code: true
          -%}
        </div>
        {% unless product.has_only_default_variant %}
          <div class="cart-upsell-item__variants" data-variant-picker>
            {% render 'product-variant-picker-cart-upsell', product: product, product_form_id: product_form_id, section_id: section_id, is_upsell: true %}
          </div>
        {% endunless %}
        <div class="cart-upsell-item__button" data-product-form>
          <product-form
            class="product-form"
            data-section-id="{{ section.id }}"
          >
            <div class="product-form__error-message-wrapper" role="alert" hidden>
              <span class="svg-wrapper">
                {{- 'icon-error.svg' | inline_asset_content -}}
              </span>
              <span class="product-form__error-message"></span>
            </div>
      
            {%- form 'product',
              product,
              id: product_form_id,
              class: 'form',
              novalidate: 'novalidate',
              data-type: 'add-to-cart-form'
            -%}
              <input
                type="hidden"
                name="id"
                value="{{ product.selected_or_first_available_variant.id }}"
                {% if product.selected_or_first_available_variant.available == false
                  or quantity_rule_soldout
                  or product.selected_or_first_available_variant == null
                %}
                  disabled
                {% endif %}
                class="product-variant-id"
              >
              <div class="product-form__buttons">
                {%- liquid
                  assign check_against_inventory = true
                  if product.selected_or_first_available_variant.inventory_management != 'shopify' or product.selected_or_first_available_variant.inventory_policy == 'continue'
                    assign check_against_inventory = false
                  endif
                  if product.selected_or_first_available_variant.quantity_rule.min > product.selected_or_first_available_variant.inventory_quantity and check_against_inventory
                    assign quantity_rule_soldout = true
                  endif
                -%}
                <button
                  id="ProductSubmitButton-{{ section_id }}-{{ product.id }}"
                  type="submit"
                  name="add"
                  class="product-form__submit button button--full-width button--primary"
                  {% if product.selected_or_first_available_variant.available == false
                    or quantity_rule_soldout
                    or product.selected_or_first_available_variant == null
                  %}
                    disabled
                  {% endif %}
                >
                  <span>
                    {%- if product.selected_or_first_available_variant == null -%}
                      {{ 'products.product.unavailable' | t }}
                    {%- elsif product.selected_or_first_available_variant.available == false or quantity_rule_soldout -%}
                      {{ 'products.product.sold_out' | t }}
                    {%- else -%}
                      {{ 'products.product.add_to_cart' | t }}
                    {%- endif -%}
                  </span>
                  {%- render 'loading-spinner' -%}
                </button>
              </div>
            {%- endform -%}
          </product-form>
        </div>
      </div>
    </div>
  </div>
</product-info-cart-upsell>

Create new liquid section cart-drawer-upsell-product.liquid

{% comment %}
  Section: cart-upsell-product
  This section renders the product-info-cart-upsell component for the given product.
{% endcomment %}

{% render 'cart-drawer-upsell-product', product: product, section_id: 'cart-drawer-upsell-product' %}

Create new snippet product-variant-picker-cart-upsell.liquid

{% comment %}
  Renders product variant-picker

  Accepts:
  - product: {Object} product object.
  - block: {Object} passing the block information.
  - product_form_id: {String} Id of the product form to which the variant picker is associated.
  Usage:
  {% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %}
{% endcomment %}

{%- unless product.has_only_default_variant -%}
    <variant-selects-cart-upsell
      id="variant-selects-{{ section.id }}-{{ product.id }}"
      data-section="{{ section.id }}"
      data-product-id="{{ product.id }}"
      data-is-upsell="true"
    >
      {%- for option in product.options_with_values -%}
        {%- liquid
          assign swatch_count = option.values | map: 'swatch' | compact | size
          assign picker_type = settings.cart_drawer_recommendation_picker_type
          assign swatch_shape = settings.cart_drawer_recommendation_swatch_shape

          if swatch_count > 0 and swatch_shape != 'none'
            if picker_type == 'dropdown'
              assign picker_type = 'swatch_dropdown'
            else
              assign picker_type = 'swatch'
            endif
          endif
        -%}
        {%- if picker_type == 'swatch' -%}
          <fieldset class="js product-form__input product-form__input--swatch">
            <legend class="form__label">
              {{ option.name }}:
              <span data-selected-value>
                {{- option.selected_value -}}
              </span>
            </legend>
            {% render 'product-variant-options',
              product: product,
              option: option,
              block: block,
              picker_type: picker_type,
              is_upsell: is_upsell
            %}
          </fieldset>
        {%- elsif picker_type == 'button' -%}
          <fieldset class="js product-form__input product-form__input--pill">
            <legend class="form__label">{{ option.name }}</legend>
            {% render 'product-variant-options',
              product: product,
              option: option,
              block: block,
              picker_type: picker_type,
              is_upsell: is_upsell
            %}
          </fieldset>
        {%- else -%}
          <div class="product-form__input product-form__input--dropdown">
            <label class="form__label" for="Option-{{ section.id }}-{{ product.id }}-{{ forloop.index0 }}">
              {{ option.name }}
            </label>
            <div class="select">
              {%- if picker_type == 'swatch_dropdown' -%}
                <span
                  data-selected-value
                  class="dropdown-swatch"
                >
                  {% render 'swatch', swatch: option.selected_value.swatch, shape: swatch_shape %}
                </span>
              {%- endif -%}
              <select
                id="Option-{{ section.id }}-{{ product.id }}-{{ forloop.index0 }}"
                class="select__select"
                name="options[{{ option.name | escape }}]"
                form="{{ product_form_id }}"
              >
                {% render 'product-variant-options',
                  product: product,
                  option: option,
                  block: block,
                  picker_type: picker_type,
                  is_upsell: is_upsell
                %}
              </select>
              <span class="svg-wrapper">
                {{- 'icon-caret.svg' | inline_asset_content -}}
              </span>
            </div>
          </div>
        {%- endif -%}
      {%- endfor -%}

      <script type="application/json" data-selected-variant>
        {{ product.selected_or_first_available_variant | json }}
      </script>

      <script type="application/json" id="ProductJSON-{{ product.id }}">
        {{ product | json }}
      </script>
    </variant-selects-cart-upsell>

{%- endunless -%}

Edit price.liquid

  • Replace settings.currency_code_enabled with currency_code_enabled
  • Add variable assignment for currency_code_enabled
  if disable_cart_upsell_currency_code
    assign currency_code_enabled = false
  else
    assign currency_code_enabled = settings.currency_code_enabled
  endif

Edit product-variant-options.liquid

  • Add variable assignment
  assign is_upsell = is_upsell | default: false
  if is_upsell
    assign product_form_id = product_form_id | append: '-' | append: product.id
  endif
  if block != null
    assign swatch_shape = block.settings.swatch_shape
  else
    assign swatch_shape = settings.cart_drawer_recommendation_swatch_shape
  endif
  • Add {% if is_upsell %}-{{ product.id }}{% endif %} to input_id and input_name
{{ section.id }}{% if is_upsell %}-{{ product.id }}{% endif %}-{{ option.position }}-{{ forloop.index0 }}
{{ option.name }}-{{ option.position }}{% if is_upsell %}-{{ product.id }}{% endif %}
  • Replace block.settings.swatch_shape with swatch_shape

Create new asset file cart-drawer-recommendations.js

class ComprehensiveCartRecommendations extends HTMLElement {
  constructor() {
      super();
      this.weightQuantity = this.parseWeight(this.dataset.weightQuantity, 0.3);
      this.weightFrequency = this.parseWeight(this.dataset.weightFrequency, 0.4);
      this.weightPosition = this.parseWeight(this.dataset.weightPosition, 0.3);
  }

  parseWeight(value, defaultValue) {
      const parsed = parseFloat(value);
      return isNaN(parsed) ? defaultValue : parsed;
  }

  connectedCallback() {
      try {
          const cartItemsString = this.dataset.cartItems.replace(/&quot;/g, '"');
          this.cartItems = JSON.parse(cartItemsString);
          this.combinedCartItems = this.combineVariants(this.cartItems);
          this.maxPosition = parseInt(this.dataset.maxPosition, 10) || 4;
          this.productsToShow = parseInt(this.dataset.productsToShow, 10) || this.maxPosition;
      } catch (e) {
          this.cartItems = [];
          this.combinedCartItems = [];
          this.maxPosition = 4;
          this.productsToShow = 4;
      }
  
      if (this.combinedCartItems.length > 0) {
          this.initializeRecommendations();
      }
  }

  combineVariants(cartItems) {
      const combinedItems = {};
      cartItems.forEach(item => {
          const productId = item.product_id;
          if (combinedItems[productId]) {
              combinedItems[productId].quantity += item.quantity;
          } else {
              combinedItems[productId] = { ...item };
          }
      });
      return Object.values(combinedItems);
  }

  initializeRecommendations() {
      this.loadRecommendations();
  }

  loadRecommendations() {
      const productIds = this.combinedCartItems.map(item => item.product_id);
      const recommendationPromises = productIds.map(id => this.fetchRecommendationsForProduct(id));
      
      Promise.all(recommendationPromises)
          .then(allRecommendations => {
              const processedRecommendations = this.processRecommendations(allRecommendations, productIds);
              this.renderRecommendations(processedRecommendations);
          })
          .catch(error => {
              console.error('Error loading recommendations:', error);
          });
  }

  async fetchRecommendationsForProduct(productId) {
      const url = `${this.dataset.url}&product_id=${productId}&section_id=${this.dataset.sectionId}`;
      try {
          const response = await fetch(url);
          const text = await response.text();
          const html = document.createElement('div');
          html.innerHTML = text;
          const productCards = html.querySelectorAll('.cart-drawer__recommendations .grid__item');
          return Array.from(productCards).map(card => this.extractProductData(card));
      } catch (error) {
          console.error(`Error fetching recommendations for product ${productId}:`, error);
          return [];
      }
  }

  extractProductData(card) {
      const productInfoElement = card.querySelector('product-info-cart-upsell');
      if (productInfoElement) {
          const productId = productInfoElement.dataset.productId;
          return {
              id: productId,
              element: card.cloneNode(true)
          };
      }
      return null;
  }

  processRecommendations(allRecommendations, sourceProductIds) {
      const totalCartQuantity = this.combinedCartItems.reduce((sum, item) => sum + item.quantity, 0);
      const totalCartItems = this.combinedCartItems.length;
      const maxPosition = this.maxPosition;

      const processedRecommendations = [];
      allRecommendations.forEach((recommendations, index) => {
          const sourceProductId = sourceProductIds[index];
          const sourceCartItem = this.combinedCartItems.find(item => item.product_id === sourceProductId);
          const sourceQuantity = sourceCartItem ? sourceCartItem.quantity : 1;

          recommendations.forEach((rec, position) => {
              if (!rec) return;
              const existingRec = processedRecommendations.find(r => r.id === rec.id);
              const invertedPosition = maxPosition + 1 - (position + 1);

              if (existingRec) {
                  existingRec.count++;
                  existingRec.recommendedBy.push({
                      productId: sourceProductId,
                      position: position + 1,
                      quantity: sourceQuantity,
                      invertedPosition: invertedPosition
                  });
              } else {
                  processedRecommendations.push({
                      ...rec,
                      count: 1,
                      recommendedBy: [{
                          productId: sourceProductId,
                          position: position + 1,
                          quantity: sourceQuantity,
                          invertedPosition: invertedPosition
                      }],
                      originalPosition: null
                  });
              }
          });
      });

      processedRecommendations.forEach((rec, index) => {
          rec.originalPosition = index + 1;
      });

      processedRecommendations.forEach((rec) => {
          rec.totalQuantity = rec.recommendedBy.reduce((sum, item) => sum + item.quantity, 0);
          rec.timesRecommended = rec.count;
          rec.avgInvertedPosition = rec.recommendedBy.reduce((sum, item) => sum + item.invertedPosition, 0) / rec.recommendedBy.length;

          const { score, Q_p_norm, N_p_norm, P_p_norm } = this.calculateScore(rec, totalCartQuantity, totalCartItems, maxPosition);

          rec.score = score;
          rec.Q_p_norm = Q_p_norm;
          rec.N_p_norm = N_p_norm;
          rec.P_p_norm = P_p_norm;
      });

      processedRecommendations.sort((a, b) => b.score - a.score);

      return processedRecommendations;
  }

  calculateScore(recommendation, totalCartQuantity, totalCartItems, maxPosition) {
      const Q_p_norm = recommendation.totalQuantity / totalCartQuantity;
      const N_p_norm = recommendation.timesRecommended / totalCartItems;
      const P_p_norm = recommendation.avgInvertedPosition / maxPosition;

      const w_Q = this.weightQuantity;
      const w_N = this.weightFrequency;
      const w_P = this.weightPosition;

      const score = (w_Q * Q_p_norm) + (w_N * N_p_norm) + (w_P * P_p_norm);

      return {
          score,
          Q_p_norm,
          N_p_norm,
          P_p_norm
      };
  }

  renderRecommendations(recommendations) {
      const productsToShow = parseInt(this.dataset.productsToShow, 10) || recommendations.length;
      const limitedRecommendations = recommendations.slice(0, productsToShow);
      
      const container = document.createElement('div');
      container.className = 'product-recommendations no-markers';
      
      limitedRecommendations.forEach((item) => {
        if (item && item.element) {
          container.appendChild(item.element);
        }
      });
      
      this.innerHTML = '';
      this.appendChild(container);
      
      const recommendationsContainer = this.closest('.cart-drawer__recommendations');
      if (recommendationsContainer) {
        if (limitedRecommendations.length > 0) {
          recommendationsContainer.classList.add('has-recommendations');
        } else {
          recommendationsContainer.classList.remove('has-recommendations');
        }
      }
      
      if (limitedRecommendations.length > 0) {
        this.classList.add('product-recommendations--loaded');
      } else {
        this.classList.remove('product-recommendations--loaded');
      }
  
      setTimeout(() => {
        this.dispatchEvent(new CustomEvent('cart-recommendations-rendered', { bubbles: true }));
      }, 0);
    }
}

customElements.define('comprehensive-cart-recommendations', ComprehensiveCartRecommendations);

class ProductInfoCartUpsell {
  static instance = null;

  constructor() {
    if (ProductInfoCartUpsell.instance) {
      return ProductInfoCartUpsell.instance;
    }
    ProductInfoCartUpsell.instance = this;

    this.handleRecommendationsRendered = this.handleRecommendationsRendered.bind(this);
    this.eventHandlers = new Map();
    this.initialize();
  }

  initialize() {
    this.container = document.querySelector('.cart-drawer__recommendations');
    if (!this.container) {
      return;
    }
    this.initProductElements();
    this.addEventListeners();
  }

  removeEventListeners() {
    if (this.productElements) {
      this.productElements.forEach((productElement) => {
        this.removeEventListenersFromElement(productElement);
      });
    }

    document.removeEventListener('cart-recommendations-rendered', this.handleRecommendationsRendered);
  }

  removeEventListenersFromElement(element) {
    const variantSelects = element.querySelector('variant-selects-cart-upsell');
    if (variantSelects) {
      const handler = this.eventHandlers.get(variantSelects);
      if (handler) {
        variantSelects.removeEventListener('cart-upsell-variant-change', handler);
        this.eventHandlers.delete(variantSelects);
      }
    }
    const productForm = element.querySelector('product-form');
    if (productForm) {
      const handler = this.eventHandlers.get(productForm);
      if (handler) {
        productForm.removeEventListener('submit', handler);
        this.eventHandlers.delete(productForm);
      }
    }
  }

  initProductElements() {
    this.removeEventListeners();
    this.productElements = this.container.querySelectorAll('.cart-upsell-item');
    this.productElements.forEach((productElement) => {
      this.initializeProductHandlers(productElement);
    });
  }

  addEventListeners() {
    document.addEventListener('cart-recommendations-rendered', this.handleRecommendationsRendered);
  }

  handleRecommendationsRendered() {
    this.initialize();
  }

  initializeProductHandlers(productElement) {
    const variantSelects = productElement.querySelector('variant-selects-cart-upsell');
    if (variantSelects) {
      const variantChangeHandler = this.handleVariantChange.bind(this, productElement);
      this.eventHandlers.set(variantSelects, variantChangeHandler);
      variantSelects.addEventListener('cart-upsell-variant-change', variantChangeHandler);
    }

    const productForm = productElement.querySelector('product-form');
    if (productForm) {
      const addToCartHandler = this.handleAddToCart.bind(this, productElement);
      this.eventHandlers.set(productForm, addToCartHandler);
      productForm.addEventListener('submit', addToCartHandler);
    }
  }

  handleVariantChange(productElement, event) {
    const variantSelects = event.target.closest('variant-selects-cart-upsell');

    if (variantSelects) {
      const selectedOptions = event.detail.selectedOptionValues;
      const variantData = this.getVariantIdByOptions(productElement, selectedOptions);

      if (variantData) {
        const productHandle = productElement.dataset.productHandle;
        this.renderProductInfo(productElement, productHandle, variantData.id);
      }
    }
  }

  getSelectedOptions(variantSelects) {
      const optionElements = variantSelects.querySelectorAll('[name^="options["]');
      const selectedOptions = [];
    
      optionElements.forEach((element) => {
        if (element.tagName === 'SELECT') {
          selectedOptions.push(element.value);
        } else if (element.tagName === 'INPUT' && element.type === 'radio' && element.checked) {
          selectedOptions.push(element.value);
        }
      });
    
      return selectedOptions;
    }
    
  getVariantIdByOptions(productElement, selectedOptions) {
      const selector = '#ProductJSON-' + productElement.dataset.productId;
      const variantJson = productElement.querySelector(selector);
    
      if (!variantJson) {
        return null;
      }
    
      const productData = JSON.parse(variantJson.textContent);
      const variant = productData.variants.find((v) =>
        selectedOptions.every((option, index) => v.options[index] === option)
      );
    
      return variant ? { id: variant.id, variant: variant } : null;
    }
    
  renderProductInfo(productElement, productHandle, variantId) {
    const url = this.buildProductUrl(productHandle, variantId);

    fetch(url)
      .then((response) => response.text())
      .then((responseText) => {
        const html = new DOMParser().parseFromString(responseText, 'text/html');
        const newProductInfo = html.querySelector('product-info-cart-upsell');
        if (newProductInfo) {
          this.updateUpsellProductInfo(productElement, newProductInfo);
        }
      })
      .catch((error) => {
        console.error('Error updating product info:', error);
      });
  }

  buildProductUrl(productHandle, variantId) {
    const params = new URLSearchParams({
      variant: variantId,
      section_id: 'cart-drawer-upsell-product'
    });

    return `/products/${productHandle}?${params.toString()}`;
  }

  updateUpsellProductInfo(oldProductElement, newProductElement) {
    const productId = oldProductElement.dataset.productId;
    const sectionId = oldProductElement.dataset.section;

    this.removeEventListenersFromElement(oldProductElement);

    oldProductElement.innerHTML = newProductElement.innerHTML;

    oldProductElement.dataset.productId = productId;
    oldProductElement.dataset.section = sectionId;

    this.initializeProductHandlers(oldProductElement);
  }

  handleAddToCart(productElement, event) {
    document.addEventListener('cart:update', (event) => {
      // Handle any additional actions after cart update
    }, { once: true });
  }
}

new ProductInfoCartUpsell();

class VariantSelectsCartUpsell extends VariantSelects {
  constructor() {
    super();
  }

  connectedCallback() {
    this.addEventListener('change', (event) => {
      const target = this.getInputForEventTarget(event.target);
      this.updateSelectionMetadata(event);

      this.dispatchEvent(
        new CustomEvent('cart-upsell-variant-change', {
          bubbles: false,
          detail: {
            event,
            target,
            selectedOptionValues: this.selectedOptionValues,
          },
        })
      );
    });
  }

get selectedOptionValues() {
    const selectedOptions = [];

    this.querySelectorAll('select').forEach((select) => {
      selectedOptions.push(select.value);
    });

    this.querySelectorAll('input[type="radio"]:checked').forEach((input) => {
      selectedOptions.push(input.value);
    });

    return selectedOptions;
  }
}

customElements.define('variant-selects-cart-upsell', VariantSelectsCartUpsell);

Edit CartItems class in cart.js

Edit method constructor

      if (!event.target.closest('.cart-upsell-product-container') && !event.target.closest('product-info-cart-upsell')) {
        this.onChange(event);
      }

Edit method resetQuantityInput

  resetQuantityInput(id) {
    const input = this.querySelector(`#Quantity-${id}`);
    if (input && !input.closest('.cart-upsell-product-container') && !input.closest('product-info-cart-upsell')) {
      const value = input.getAttribute('value');
      if (value !== null) {
        input.value = value;
      }
      this.isEnterPressed = false;
    }
  }

Edit method setValidity

  setValidity(event, index, message) {
    if (event.target.setCustomValidity) {
      event.target.setCustomValidity(message);
      event.target.reportValidity();
    }
    this.resetQuantityInput(index);
    if (event.target.select && typeof event.target.select === 'function') {
      event.target.select();
    }
  }

Edit method validateQuantity

    if (event.target.closest('.cart-upsell-product-container') || event.target.closest('product-info-cart-upsell')) {
      return;
    }

Edit theme.liquid

  {% if settings.enable_cart_upsell %}
    <script src="{{ 'product-form.js' | asset_url }}" defer="defer"></script>
    <script src="{{ 'cart-drawer-recommendations.js' | asset_url }}" defer="defer"></script>
  {% endif %}

Browse other ways to boost conversion rate & profit