How to Add a Free Shipping Progress Bar to Shopify Cart

2026-09-25 · shipping fulfillment

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:

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:

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:

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:

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.