Skip to content
Multi-Step Cart Progress Bar - Free Tutorial
Browse other ways to boost conversion rate & profit

Multi-Step Cart Progress Bar - Free Tutorial

In this tutorial, we’ll show you how to set up a multi-step cart progress bar to motivate customers to buy more. This feature is an effective way to increase your average order value (AOV) at various price points, providing a revenue-boosting incentive for all buyers.

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

 

Create metafield

Create new market metafields

  • Name: Cart Progress Goal Threshold 1
    • Namespace and key: custom.cart_progress_goal_threshold_1
    • Type: Decimal (one value)
    • Minimum value: 0
    • Maximum decimal places: 2
  • Name: Cart Progress Goal Threshold 2
    • Namespace and key: custom.cart_progress_goal_threshold_2
    • Type: Decimal (one value)
    • Minimum value: 0
    • Maximum decimal places: 2
  • Name: Cart Progress Goal Threshold 3
    • Namespace and key: custom.cart_progress_goal_threshold_3
    • Type: Decimal (one value)
    • Minimum value: 0
    • Maximum decimal places: 2

Edit Theme Code

Add settings to settings_schema.json

  {
    "name": "Cart Progress Bar",
    "settings": [
      {
        "type": "checkbox",
        "id": "enable_cart_progress_bar",
        "label": "Enable Cart Progress Bar",
        "default": false
      },
      {
        "type": "checkbox",
        "id": "show_progress_bar_icons",
        "label": "Show progress bar icons",
        "default": false
      },
      {
        "type": "header",
        "content": "Goal 1"
      },
      {
        "type": "checkbox",
        "id": "enable_goal_1",
        "label": "Enable Goal 1",
        "default": true
      },
      {
        "type": "text",
        "id": "goal_1_pre_goal_message",
        "label": "Goal 1 Pre-Goal Message",
        "default": "You're only [remaining_for_goal] away from <strong>FREE SHIPPING</strong>",
        "info": "Use [remaining_for_goal] to insert the dynamic remaining amount."
      },
      {
        "type": "text",
        "id": "goal_1_post_goal_message",
        "label": "Goal 1 Post-Goal Message",
        "default": "🎉 Congrats! You've unlocked <strong>FREE SHIPPING</strong>",
        "info": "Message displayed after reaching Goal 1."
      },
      {
        "type": "text",
        "id": "goal_1_description",
        "label": "Goal 1 Description",
        "default": "Free Shipping",
        "info": "Text displayed beneath Goal 1 icon."
      },
      {
        "type": "image_picker",
        "id": "goal_1_icon",
        "label": "Goal 1 Icon"
      },
      {
        "type": "image_picker",
        "id": "goal_1_icon_reached",
        "label": "Goal 1 Reached Icon"
      },
      {
        "type": "header",
        "content": "Goal 2"
      },
      {
        "type": "checkbox",
        "id": "enable_goal_2",
        "label": "Enable Goal 2",
        "default": false
      },
      {
        "type": "text",
        "id": "goal_2_pre_goal_message",
        "label": "Goal 2 Pre-Goal Message",
        "default": "You're only [remaining_for_goal] away from a <strong>FREE GIFT</strong>",
        "info": "Use [remaining_for_goal] to insert the dynamic remaining amount."
      },
      {
        "type": "text",
        "id": "goal_2_post_goal_message",
        "label": "Goal 2 Post-Goal Message",
        "default": "🎉 Congrats! You've unlocked your <strong>FREE GIFT</strong>",
        "info": "Message displayed after reaching Goal 2."
      },
      {
        "type": "text",
        "id": "goal_2_description",
        "label": "Goal 2 Description",
        "default": "Free Gift",
        "info": "Text displayed beneath Goal 2 icon."
      },
      {
        "type": "image_picker",
        "id": "goal_2_icon",
        "label": "Goal 2 Icon"
      },
      {
        "type": "image_picker",
        "id": "goal_2_icon_reached",
        "label": "Goal 2 Reached Icon"
      },
      {
        "type": "header",
        "content": "Goal 3"
      },
      {
        "type": "checkbox",
        "id": "enable_goal_3",
        "label": "Enable Goal 3",
        "default": false
      },
      {
        "type": "text",
        "id": "goal_3_pre_goal_message",
        "label": "Goal 3 Pre-Goal Message",
        "default": "You're only [remaining_for_goal] away from <strong>20% OFF</strong>",
        "info": "Use [remaining_for_goal] to insert the dynamic remaining amount."
      },
      {
        "type": "text",
        "id": "goal_3_post_goal_message",
        "label": "Goal 3 Post-Goal Message",
        "default": "🎉 Congrats! You've unlocked <strong>20% OFF</strong>",
        "info": "Message displayed after reaching Goal 3."
      },
      {
        "type": "text",
        "id": "goal_3_description",
        "label": "Goal 3 Description",
        "default": "20% OFF",
        "info": "Text displayed beneath Goal 3 icon."
      },
      {
        "type": "image_picker",
        "id": "goal_3_icon",
        "label": "Goal 3 Icon"
      },
      {
        "type": "image_picker",
        "id": "goal_3_icon_reached",
        "label": "Goal 3 Reached Icon"
      },
      {
        "type": "header",
        "content": "Formatting Options"
      },
      {
        "type": "color",
        "id": "cart_progress_bar_color",
        "label": "Progress Bar Color",
        "default": "#d53600"
      },
      {
        "type": "color",
        "id": "cart_progress_bar_full_color",
        "label": "Progress Bar Full Color",
        "default": "#d53600"
      },
      {
        "type": "color",
        "id": "cart_progress_bar_background_color",
        "label": "Progress Bar Background Color",
        "default": "#eee"
      },
      {
        "type": "color",
        "id": "cart_progress_bar_text_color",
        "label": "Progress Bar Text Color",
        "default": "#333"
      }
    ]
  }

Create a new snippet cart-progress-bar-custom.liquid

{% if cart != empty and settings.enable_cart_progress_bar %}
  {% if localization.market.metafields.custom.cart_progress_goal_threshold_1 != nil or localization.market.metafields.custom.cart_progress_goal_threshold_2 != nil or localization.market.metafields.custom.cart_progress_goal_threshold_3 != nil %} 
  
    {% assign cart_total_cents = cart.total_price %}
    {%- assign thresholds = '' -%}
    {%- assign pre_goal_messages = '' -%}
    {%- assign post_goal_messages = '' -%}
    {%- assign descriptions = '' -%}
    {%- assign goal_positions = '' -%}
  
    {%- assign previous_threshold = 0 -%}
    {%- assign preload_images = '' -%} 
  
    {% for i in (1..3) %}
      {% assign enable_goal_key = 'enable_goal_' | append: i %}
      {% assign enable_goal = settings[enable_goal_key] %}
      {% if enable_goal %}
        {%- assign metafield_name = 'cart_progress_goal_threshold_' | append: i -%}
        {%- assign current_metafield = localization.market.metafields.custom[metafield_name] -%}
        
        {% if current_metafield != blank %}
          {%- assign potential_threshold = current_metafield | times: 100 | round -%}
          
          {% if i == 1 or potential_threshold > previous_threshold %}
            {%- assign threshold = potential_threshold -%}
            {%- assign previous_threshold = threshold -%}
        
            {% assign pre_goal_message_key = 'goal_' | append: i | append: '_pre_goal_message' %}
            {% assign pre_goal_message = settings[pre_goal_message_key] %}
  
            {% assign post_goal_message_key = 'goal_' | append: i | append: '_post_goal_message' %}
            {% assign post_goal_message = settings[post_goal_message_key] %}
  
            {% assign description_key = 'goal_' | append: i | append: '_description' %}
            {% assign description = settings[description_key] %}
  
            {%- if thresholds != '' -%}
              {%- assign thresholds = thresholds | append: ',' -%}
              {%- assign pre_goal_messages = pre_goal_messages | append: '||' -%}
              {%- assign post_goal_messages = post_goal_messages | append: '||' -%}
              {%- assign descriptions = descriptions | append: '||' -%}
            {%- endif -%}
  
            {%- assign thresholds = thresholds | append: threshold -%}
            {%- assign pre_goal_messages = pre_goal_messages | append: pre_goal_message -%}
            {%- assign post_goal_messages = post_goal_messages | append: post_goal_message -%}
            {%- assign descriptions = descriptions | append: description -%}
          {% endif %}
        {% endif %}
      {% endif %}
    {% endfor %}
  
    {% assign thresholds = thresholds | split: ',' %}
    {% assign pre_goal_messages = pre_goal_messages | split: '||' %}
    {% assign post_goal_messages = post_goal_messages | split: '||' %}
    {% assign descriptions = descriptions | split: '||' %}
    {% assign total_threshold_cents = thresholds.last | plus: 0 %}
    {% assign progress_fraction = cart_total_cents | times: 100 | divided_by: total_threshold_cents %}
  
    {% if progress_fraction > 100 %}
      {% assign progress_percentage = 100 %}
    {% else %}
      {% assign progress_percentage = progress_fraction %}
    {% endif %}
  
    {% for threshold in thresholds %}
      {% assign goal_position = threshold | times: 100 | divided_by: total_threshold_cents %}
      {% if goal_positions != '' %}
        {% assign goal_positions = goal_positions | append: ',' %}
      {% endif %}
      {% assign goal_positions = goal_positions | append: goal_position %}
    {% endfor %}
    {% assign goal_positions = goal_positions | split: ',' %}
  
    {% assign thresholds_size = thresholds.size | minus: 1 %}
    {%- assign next_goal_index = -1 -%}
    {%- for index in (0..thresholds_size) -%}
      {% assign threshold = thresholds[index] | plus: 0 %}
      {% assign cart_total_diff = cart_total_cents | minus: threshold %}
      {% if cart_total_diff < 0 %}
        {% assign next_goal_index = index %}
        {% break %}
      {% endif %}
    {%- endfor -%}
  
    {% if next_goal_index == -1 %}
      {% assign message = post_goal_messages.last %}
    {% else %}
      {% assign pre_goal_message_template = pre_goal_messages[next_goal_index] %}
      {% assign threshold = thresholds[next_goal_index] | plus: 0 %}
      {% assign remaining_for_goal = threshold | minus: cart_total_cents %}
      {% if remaining_for_goal < 0 %}
        {% assign remaining_for_goal = 0 %}
      {% endif %}
  
      {% if settings.currency_code_enabled %}
        {% assign remaining_for_goal_formatted = remaining_for_goal | money_with_currency %}
      {% else %}
        {% assign remaining_for_goal_formatted = remaining_for_goal | money %}
      {% endif %}
      {% assign message = pre_goal_message_template | replace: '[remaining_for_goal]', remaining_for_goal_formatted %}
    {% endif %}
  
    <div id="cart-progress-wrapper"
      class="{% if progress_percentage == 100 %}full{% else %}not-full{% endif %}"
      data-thresholds="{{ thresholds | join: ',' }}"
      data-pre-goal-messages="{{ pre_goal_messages | join: '||' | escape }}"
      data-post-goal-messages="{{ post_goal_messages | join: '||' | escape }}"
      data-goal-positions="{{ goal_positions | join: ',' }}"
      data-currency-format="{% if settings.currency_code_enabled %}{{ shop.money_with_currency_format | escape }}{% else %}{{ shop.money_format | escape }}{% endif %}"
    >
      <div class="cart-progress-bar-and-icons-wrapper">
        <div class="cart-progress-bar-container">
          <div id="cart-progress-bar" 
            style="width: {{ progress_percentage }}%;"
            data-initial-width="{{ progress_percentage }}%"
          >
          </div>
        </div>
  
        {% if settings.show_progress_bar_icons %}
          <div class="goal-icons-container">
            {% assign goal_positions_size = goal_positions.size | minus: 1 %}
            {% for index in (0..goal_positions_size) %}
              {% assign goal_position = goal_positions[index] %}
              {% assign threshold = thresholds[index] | plus: 0 %}
              {% assign cart_total_diff = cart_total_cents | minus: threshold %}
              
              {% assign current_index = index | plus: 1 %}
              {% assign icon_key = 'goal_' | append: current_index | append: '_icon' %}
              {% assign icon_reached_key = 'goal_' | append: current_index | append: '_icon_reached' %}
              {% assign image_icon = settings[icon_key] %}
              {% assign image_icon_reached = settings[icon_reached_key] %}
              {% assign image_alt_text = 'Goal ' | append: current_index %}
              {% assign image_alt_text_reached = 'Goal ' | append: current_index | append: ' Reached' %}
              
              {% if image_icon != blank %}
                {% assign image_icon_url = image_icon | image_url: width: 50 %}
                {% unless preload_images contains image_icon_url %}
                  {% if preload_images != '' %}
                    {% assign preload_images = preload_images | append: ',' %}
                  {% endif %}
                  {% assign preload_images = preload_images | append: image_icon_url %}
                {% endunless %}
              {% endif %}
              {% if image_icon_reached != blank %}
                {% assign image_icon_reached_url = image_icon_reached | image_url: width: 50 %}
                {% unless preload_images contains image_icon_reached_url %}
                  {% if preload_images != '' %}
                    {% assign preload_images = preload_images | append: ',' %}
                  {% endif %}
                  {% assign preload_images = preload_images | append: image_icon_reached_url %}
                {% endunless %}
              {% endif %}
  
              <div class="goal-icon" style="left: {{ goal_position }}%;"
                {% if image_icon != blank %}data-regular-icon="{{ image_icon | image_url: width: 50 }}"{% endif %}
                {% if image_icon_reached != blank %}data-reached-icon="{{ image_icon_reached | image_url: width: 50 }}"{% endif %}
                data-index="{{ current_index }}"
              >
                {% if image_icon != blank or image_icon_reached != blank %}
                  {% if cart_total_diff < 0 %}
                    {% if image_icon != blank %}
                      {{ image_icon | image_url: width: 50 | image_tag: preload: true, alt: image_alt_text }}
                    {% endif %}
                  {% else %}
                    {% if image_icon_reached != blank %}
                      {{ image_icon_reached | image_url: width: 50 | image_tag: preload: true, alt: image_alt_text_reached }}
                    {% elsif image_icon != blank %}
                      {{ image_icon | image_url: width: 50 | image_tag: preload: true, alt: image_alt_text }}
                    {% endif %}
                  {% endif %}
                {% endif %}
                {% if descriptions[index] != blank %}
                  <div class="goal-description">{{ descriptions[index] }}</div>
                {% endif %}
                </div>
              
            {% endfor %}
          </div>
        {% endif %}
      </div>
      <div class="goal-message">
        {{ message }}
      </div>
    </div>
  
    {% assign preload_images_array = preload_images | split: ',' %}
    <div style="display:none;">
      {% for preload_image in preload_images_array %}
        <img src="{{ preload_image }}" alt="">
      {% endfor %}
    </div>
  
    <style>
      #cart-progress-bar {
        display: block;
        height: 10px;
        background-color: var(--progress-bar-color, {{ settings.cart_progress_bar_color }});
        border-radius: 10px;
        border: 1px solid var(--progress-bar-border-color, {{ settings.cart_progress_bar_color }});
        box-sizing: border-box;
      }
      
      #cart-progress-wrapper.full #cart-progress-bar {
        --progress-bar-color: {{ settings.cart_progress_bar_full_color }};
        --progress-bar-border-color: {{ settings.cart_progress_bar_full_color }};
      }
      
      {% if settings.show_progress_bar_icons %}
        .cart-progress-bar-and-icons-wrapper {
          margin-right: 20px;
          }
      {% endif %}
    
      .cart-progress-bar-container {
        width: 100%;
        background-color: {{ settings.cart_progress_bar_background_color }};
        border-radius: 10px;
        margin: 10px auto;
        padding: 1px;
        overflow: hidden;
        position: relative;
        height: 12px;
      }
    
      {% if settings.show_progress_bar_icons %}
        .goal-icons-container {
          position: relative;
          margin-top: -30px;
          height: 60px;
          pointer-events: none;
        }
        
        .goal-icon {
          position: absolute;
          top: 0;
          transform: translateX(-50%);
          text-align: center;
          margin: 0;
          padding: 0;
          line-height: 1;
          width: 50px;
          display: flex;
          flex-direction: column;
          align-items: center;
        }
        
        .goal-icon img {
          width: 30px;
          height: 30px;
          margin: 0 auto;
          display: block;
        }
        
        .goal-description {
          font-size: 12px;
          color: {{ settings.cart_progress_bar_text_color }};
          margin-top: 4px;
          text-align: center;
          width: 100%;
          overflow-wrap: break-word;
          word-wrap: break-word;
          hyphens: auto;
        }
      {% endif %}
      
      .goal-message {
        text-align: center;
        margin: 10px auto;
        font-size: 1em;
        color: {{ settings.cart_progress_bar_text_color }};
      }
    </style>
  {% endif %}
{% endif %}

Edit main-cart-items.liquid

{%- unless settings.cart_type == 'drawer' -%}
	{% render 'cart-progress-bar-custom' %}
{%- endunless -%}

Edit cart-drawer.liquid

{% render 'cart-progress-bar-custom' %}

Add a new method in cart.js

Call the new method in updateQuantity

  const updatedCartTotal = parsedState.total_price;
  this.updateProgressBar(updatedCartTotal);

New methods

updateProgressBar(cartTotalCents) {
  const progressWrapper = document.getElementById('cart-progress-wrapper');
  if (!progressWrapper) return;

  const currencyFormat = progressWrapper.dataset.currencyFormat;
  const thresholds = progressWrapper.dataset.thresholds.split(',').map(Number);
  const preGoalMessages = progressWrapper.dataset.preGoalMessages.split('||');
  const postGoalMessages = progressWrapper.dataset.postGoalMessages.split('||');
  const goalPositions = progressWrapper.dataset.goalPositions.split(',').map(Number);

  const totalThreshold = thresholds[thresholds.length - 1];
  const progressPercentage = Math.min((cartTotalCents / totalThreshold) * 100, 100);

  const progressBar = document.getElementById('cart-progress-bar');
  const goalIcons = document.querySelectorAll('.goal-icon');
  const goalMessageElement = document.querySelector('.goal-message');

  if (cartTotalCents === 0) {
    progressWrapper.style.display = 'none';
    goalMessageElement.style.display = 'none';
    progressBar.style.width = '0%'; 
  } else {
    progressWrapper.style.display = 'block';
    const previousWidth = parseFloat(progressBar.style.width) || 0;
    progressBar.style.width = `${progressPercentage}%`;

    if (progressPercentage >= 100) {
      progressWrapper.classList.add('full');
    } else {
      progressWrapper.classList.remove('full');
    }

    let nextGoalIndex = -1;
    for (let i = 0; i < thresholds.length; i++) {
      if (cartTotalCents < thresholds[i]) {
        nextGoalIndex = i;
        break;
      }
    }

    goalIcons.forEach((goalIcon, index) => {
      const cartTotalDiff = cartTotalCents - thresholds[index];
      const icon = goalIcon.querySelector('img');
      const goalNumber = goalIcon.dataset.index;
      
      if (icon) {
        if (cartTotalDiff < 0) {
          const regularIconUrl = goalIcon.dataset.regularIcon;
          if (regularIconUrl) {
            icon.src = regularIconUrl;
            icon.srcset = `${regularIconUrl} 50w`;
            icon.alt = `Goal ${goalNumber}`;
          }
        } else {
          const reachedIconUrl = goalIcon.dataset.reachedIcon;
          if (reachedIconUrl) {
            icon.src = reachedIconUrl;
            icon.srcset = `${reachedIconUrl} 50w`;
            icon.alt = `Goal ${goalNumber} Reached`;
          }
        }
      }
    });

    goalMessageElement.style.display = 'block';
    if (nextGoalIndex === -1) {
      const message = postGoalMessages[postGoalMessages.length - 1];
      goalMessageElement.innerHTML = message;
    } else {
      const remainingForGoal = thresholds[nextGoalIndex] - cartTotalCents;
      const remainingAmount = remainingForGoal / 100;
      const remainingAmountFormatted = this.formatCurrency(currencyFormat, remainingAmount);
      const preGoalMessageTemplate = preGoalMessages[nextGoalIndex];
      const message = preGoalMessageTemplate.replace('[remaining_for_goal]', remainingAmountFormatted);
      goalMessageElement.innerHTML = message;
    }
  }
}

formatCurrency(currencyFormat, amount) {
  let formattedAmount = '';
  formattedAmount = currencyFormat
    .replace('{{amount}}', amount.toFixed(2)) // Standard with two decimals
    .replace('{{amount_no_decimals}}', amount.toFixed(0)) // No decimals
    .replace('{{amount_with_comma_separator}}', amount.toFixed(2).replace('.', ',')) // Replace period with comma
    .replace('{{amount_no_decimals_with_comma_separator}}', amount.toFixed(0).replace('.', ',')) // No decimals, use comma
    .replace('{{amount_with_apostrophe_separator}}', amount.toFixed(2).replace('.', "'")) // Apostrophe separator
    .replace('{{amount_no_decimals_with_space_separator}}', amount.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ' ')) // No decimals, space
    .replace('{{amount_with_space_separator}}', amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' ').replace('.', ',')) // Space separator
    .replace('{{amount_with_period_and_space_separator}}', amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' ')); // Period and space
  return formattedAmount;
}

Browse other ways to boost conversion rate & profit