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;
}