How to Add a Free Shipping Progress Bar to Shopify Cart
A free shipping progress bar is one of the highest-ROI changes you can make to a Shopify cart. It works because it converts a vague "spend more to get free shipping" promise into a visible, numeric gap the customer can close. This guide shows you how to add one to your cart template using Liquid, CSS, and a small amount of JavaScript — no app required.
Why a Free Shipping Progress Bar Lifts Average Order Value
Free shipping is the single most effective conversion lever in e-commerce. The progress bar exploits a well-documented behavioral effect: once a customer has started toward a goal, they are more motivated to finish it. A bar that reads "You're $12 away from free shipping" does something a banner reading "Free shipping over $50" cannot — it tells the shopper exactly how close they already are.
The mechanics matter as much as the psychology. The bar should appear in the cart, where the customer is already reviewing their order and deciding whether to check out or add one more item. That's the moment of maximum influence.
Three things make a progress bar work:
- A threshold that is reachable. If your average order value is $45 and you set free shipping at $150, almost nobody will hit it and the bar becomes noise. Set the threshold roughly 15–30% above your current average order value.
- A visible numeric gap. Show the remaining dollar amount, not just a percentage. "You're $12 away" is actionable; a half-filled bar is not.
- A reward that's actually valuable. Free shipping on a $4 item isn't worth a second purchase. Free shipping over $50 is.
If you already offer free shipping on everything, this tactic doesn't apply. Use a free-gift or discount threshold instead, and the same code pattern works.
What You Need Before You Start (Theme Access and a Backup)
Before touching any theme code, confirm you have the following:
- Admin access to the Shopify store with permission to edit themes. Staff accounts without theme permissions can't do this.
- A duplicated theme. In Shopify admin, go to Online Store → Themes, click the ... menu on your live theme, and choose Duplicate. Edit the duplicate, preview it, and publish only when you're satisfied. This is your rollback plan.
- Your cart template. Most themes use
sections/main-cart.liquidortemplates/cart.liquid. Dawn, Refresh, Craft, and most modern free themes use thesections/version. Older themes may usetemplates/cart.liquid. Open the theme code editor and search for "cart" to find the right file. - Your cart total variable. In Liquid,
cart.total_pricegives the cart subtotal in cents. This is the value you'll compare against your threshold. - A free shipping threshold decision. Pick a number now. See Step 1.
If you'd rather not touch code, apps like Shipmate, Free Shipping Bar, and Hextom's Free Shipping Bar handle this. Check current pricing on the Shopify App Store before committing — free tiers exist but often cap impressions or features. The code approach below avoids monthly fees and gives you full control over styling.
Step 1: Set Your Free Shipping Threshold
Your threshold should be a business decision, not a round number you like. Pull your average order value from Shopify Analytics → Reports → Average order value over the last 90 days. Then set your threshold 15–30% higher.
| Average order value | Suggested threshold | Reasoning |
|---|---|---|
| Under $30 | $35–$40 | Small nudge; shipping cost is low on light items |
| $30–$60 | $50–$75 | Most effective range; one extra item usually clears it |
| $60–$120 | $80–$150 | Requires a deliberate second item; pair with a bundle |
| Over $120 | $150–$200 | Consider free shipping at a higher tier plus a lower-tier discount |
Also confirm the threshold is profitable. If free shipping costs you $8 and your margin on a typical add-on item is $6, a threshold that pushes customers toward low-margin items can lose money. Check your margins before you commit.
Step 2: Add the Progress Bar Markup to Your Cart Template
Open your cart template in the theme code editor. Find the element that renders the cart subtotal or the checkout button. Insert the markup just above it, inside the same container so it inherits the cart's spacing.
{%- assign threshold = 5000 -%}
{%- assign remaining = threshold | minus: cart.total_price -%}
<div class="shipping-bar" data-threshold="{{ threshold }}">
<p class="shipping-bar__message">
{%- if remaining > 0 -%}
You're <span class="shipping-bar__amount">{{ remaining | money }}</span> away from free shipping.
{%- else -%}
You've unlocked free shipping.
{%- endif -%}
</p>
<div class="shipping-bar__track">
<div class="shipping-bar__fill"
style="width: {{ cart.total_price | times: 100 | divided_by: threshold | at_most: 100 }}%">
</div>
</div>
</div>
Two details matter here:
threshold = 5000means $50.00, because Shopify stores money in cents. Change this to your number from Step 1.at_most: 100caps the fill width at 100% so a large cart doesn't overflow the bar.
The data-threshold attribute lets JavaScript update the bar without a page reload. The Liquid fallback handles the initial render, so the bar is correct even if JavaScript fails.
Step 3: Style the Bar with CSS
Add this to your theme's CSS file (usually assets/base.css or assets/theme.css), or inside a <style> block in the section. Adjust colors to match your brand.
.shipping-bar {
margin: 0 0 1.5rem;
padding: 1rem;
background: #f6f6f6;
border-radius: 6px;
}
.shipping-bar__message {
margin: 0 0 0.75rem;
font-size: 0.95rem;
}
.shipping-bar__amount {
font-weight: 700;
}
.shipping-bar__track {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.shipping-bar__fill {
height: 100%;
background: #1a7f37;
border-radius: 4px;
transition: width 0.3s ease;
}
.shipping-bar--complete .shipping-bar__fill {
background: #1a7f37;
}
Keep the bar thin (6–10px) and the message short. On mobile, the cart is narrow — a two-line message plus a bar is the maximum that reads well. Test at 375px width before you publish.
Step 4: Add the JavaScript to Track Cart Total
The Liquid render only updates on page load. When a customer changes quantity or removes an item, the cart updates via AJAX and the bar goes stale. Add this script to your theme's JavaScript file or inside a <script> tag at the bottom of the cart section.
document.addEventListener('cart:updated', updateShippingBar);
function updateShippingBar(cart) {
const bar = document.querySelector('.shipping-bar');
if (!bar) return;
const threshold = parseInt(bar.dataset.threshold, 10);
const total = cart.total_price;
const remaining = threshold - total;
const amountEl = bar.querySelector('.shipping-bar__amount');
const messageEl = bar.querySelector('.shipping-bar__message');
const fillEl = bar.querySelector('.shipping-bar__fill');
if (remaining > 0) {
messageEl.innerHTML =
"You're <span class=\"shipping-bar__amount\">" +
formatMoney(remaining) + "</span> away from free shipping.";
} else {
messageEl.textContent = "You've unlocked free shipping.";
bar.classList.add('shipping-bar--complete');
}
const percent = Math.min((total / threshold) * 100, 100);
fillEl.style.width = percent + '%';
}
function formatMoney(cents) {
return '$' + (cents / 100).toFixed(2);
}
If your theme doesn't emit a cart:updated event, hook into the fetch response instead. In Dawn-based themes, the cart is updated through sections/cart-drawer.liquid or assets/cart.js. Find the function that re-renders the cart after a quantity change and call updateShippingBar(cart) there with the cart object returned by the Shopify Cart API (/cart.js).
If you use a cart drawer, add the same markup to the drawer template and call the same function — otherwise the drawer will show stale numbers.
Step 5: Test the Bar on Desktop, Mobile, and With Discounts
Test these cases before publishing:
- Empty cart. The bar should show the full remaining amount and a 0% fill, not a broken layout.
- Cart just below threshold. Confirm the remaining amount is accurate to the cent.
- Cart just above threshold. The message should switch to the unlocked state and the bar should be full.
- Quantity change. Add an item, then increase quantity. The bar should update without a page reload.
- Discount codes. Apply a code and check whether the bar uses the pre-discount or post-discount total.
cart.total_pricereflects discounts, so the bar may jump backward — decide whether that's acceptable or usecart.items_subtotal_priceinstead. - Mobile at 375px. Check that the message doesn't wrap awkwardly and the bar stays visible above the checkout button.
- Multiple currencies. If you sell internationally, confirm the money format renders correctly in each currency.
Common Issues and How to Fix Them
The bar doesn't update when quantity changes. Your theme isn't firing cart:updated, or it's firing before the cart object is ready. Log the cart object in the console and call updateShippingBar from the same place your theme re-renders the cart.
The bar shows the wrong amount. You're mixing cents and dollars. Shopify's cart.total_price is in cents; your threshold must also be in cents. A threshold of 50 means $0.50, not $50.
The bar overflows on large carts. Add at_most: 100 to the Liquid calculation and Math.min(..., 100) in JavaScript.
The bar is invisible in the cart drawer. You added the markup to the cart page but not the drawer. Duplicate the markup and the update call into the drawer template.
The bar breaks after a theme update. Theme updates overwrite customized files. Keep a copy of your changes and reapply them, or move the markup into a separate section file that isn't touched by updates.
Discounts push the customer back below the threshold. Switch from cart.total_price to cart.items_subtotal_price if you want the bar to reflect the pre-discount subtotal. Be aware this can confuse customers who expect discounts to count.
A free shipping progress bar is a small piece of code with an outsized effect on average order value, and building it yourself keeps you off a monthly app subscription. Set a threshold based on your real numbers, duplicate your theme before editing, and test the bar against discounts and mobile before you publish. Done carefully, it's a one-afternoon change that pays for itself quickly.
FAQ
Does this work on any Shopify theme?
It works on any theme where you can edit Liquid and CSS. The file paths differ — Dawn-family themes use sections/main-cart.liquid, older themes use templates/cart.liquid — but the Liquid variables (cart.total_price, cart.items_subtotal_price) are the same across themes.
Will this slow down my store? No. The markup and CSS add a few kilobytes, and the JavaScript is a few dozen lines. There's no external request and no app script loading in the background, so the performance impact is negligible compared to a third-party app.
Can I show a different threshold for different customer groups?
Yes, with Liquid conditionals. Wrap the threshold assignment in a check on customer.tags or customer.id to offer a lower threshold to wholesale or VIP customers. You'll need to pass the same logic into the JavaScript, or render the threshold into the data-threshold attribute so the script reads the correct value per customer.