Disclosure: I work on Brix, a Shopify cart app. This post shows how to build the feature yourself, and Brix is mentioned only at the end.
A free shipping progress bar is one of the simplest cart features to add, and it works because it makes a goal visible: "You're $12 away from free shipping." In this post we will build one with a Liquid snippet, a little CSS and a short script that keeps it in step with the cart.
It is written for a standard Online Store 2.0 theme such as Dawn. Themes differ, so treat the code as a starting point and test it in your own theme.
What we are building
- A bar that fills as the cart total approaches a threshold.
- A message that says how much more to spend, then switches to a success message.
- Correct on first load (rendered by Liquid), and updated live when items are added, changed or removed.
- Accessible: it exposes progress to screen readers and announces changes politely.
1. The Liquid snippet
Create snippets/free-shipping-bar.liquid. It takes a threshold in your store currency (for example 75) and renders the first state on the server, so there is no flash of an empty bar.
{%- liquid
assign threshold_cents = threshold | times: 100
assign total = cart.total_price
assign remaining = threshold_cents | minus: total
if remaining < 0
assign remaining = 0
endif
assign percent = total | times: 100.0 | divided_by: threshold_cents | round
if percent > 100
assign percent = 100
endif
-%}
<div class="fsb" data-fsb data-threshold="{{ threshold_cents }}">
<p class="fsb__message" data-fsb-message aria-live="polite">
{%- if remaining == 0 -%}
You've unlocked free shipping.
{%- else -%}
You're {{ remaining | money }} away from free shipping.
{%- endif -%}
</p>
<div
class="fsb__track"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="{{ percent }}"
aria-label="Progress toward free shipping"
>
<div class="fsb__fill" data-fsb-fill style="width: {{ percent }}%"></div>
</div>
</div>
Render it wherever the cart total is visible, for example in your cart page section or cart drawer:
{% render 'free-shipping-bar', threshold: 75 %}
2. The CSS
Add this to your theme's stylesheet (or a new asset). The width transition gives a smooth fill.
.fsb { margin: 0 0 1rem; }
.fsb__message { margin: 0 0 .5rem; font-size: .9rem; }
.fsb__track {
height: 8px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
}
.fsb__fill {
height: 100%;
background: #16a34a;
border-radius: 999px;
transition: width .3s ease;
}
@media (prefers-reduced-motion: reduce) {
.fsb__fill { transition: none; }
}
3. The JavaScript
Create assets/free-shipping-bar.js and load it with defer. It does two jobs: update the bar from /cart.js, and run that update after any cart-changing request.
(() => {
const bar = document.querySelector('[data-fsb]');
if (!bar) return;
const threshold = Number(bar.dataset.threshold); // in cents
const fill = bar.querySelector('[data-fsb-fill]');
const message = bar.querySelector('[data-fsb-message]');
const track = bar.querySelector('[role="progressbar"]');
const currency =
(window.Shopify && Shopify.currency && Shopify.currency.active) || 'USD';
const money = new Intl.NumberFormat(document.documentElement.lang || undefined, {
style: 'currency',
currency,
});
function render(totalCents) {
const remaining = Math.max(threshold - totalCents, 0);
const percent = Math.min(Math.round((totalCents / threshold) * 100), 100);
fill.style.width = percent + '%';
track.setAttribute('aria-valuenow', percent);
message.textContent =
remaining === 0
? "You've unlocked free shipping."
: `You're ${money.format(remaining / 100)} away from free shipping.`;
}
async function refresh() {
const res = await fetch('/cart.js', { headers: { Accept: 'application/json' } });
const cart = await res.json();
render(cart.total_price);
}
// Run after any request that changes the cart.
const originalFetch = window.fetch;
window.fetch = function (...args) {
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
const request = originalFetch.apply(this, args);
if (/\/cart\/(add|change|update|clear)/.test(url)) {
request.then(refresh).catch(() => {});
}
return request;
};
})();
Load it in layout/theme.liquid, before </body>:
<script src="{{ 'free-shipping-bar.js' | asset_url }}" defer></script>
How it works
- First paint: Liquid works out the remaining amount and the percentage, so the bar is right before any JavaScript runs.
-
Updates: wrapping
fetchlets the bar refresh after the cart's add, change, update and clear requests, which is what most modern themes use. The script then asks/cart.jsfor the current total and redraws. -
Accessibility:
role="progressbar"witharia-valuenowexposes the progress, andaria-live="polite"announces the new message without interrupting. - Motion: the fill animates unless the visitor prefers reduced motion.
Limits and things to check
-
Themes that use XHR instead of
fetch. Older themes may not be covered by the wrapper. If so, listen for your theme's own cart event instead, or callrefresh()from the theme's cart update function. -
Multiple currencies. The threshold above is in your store currency. If you sell in other currencies, you need to convert it (Shopify exposes a rate on
Shopify.currency.ratein many themes), then test it in each currency. - Currencies without decimals. The code assumes the usual 100 minor units. Check currencies such as JPY in your own store.
- Shipping rules live elsewhere. The bar only displays a goal. The actual free shipping rule must match it in your shipping settings.
- Choose the threshold from data. A threshold slightly above your current average order value tends to feel reachable. I wrote about how to pick the number here: how to set the right free shipping threshold.
Test it
On a development store with Dawn:
- Add the snippet, CSS and script, and render the snippet in the cart section.
- Add a product under the threshold. The bar should be partly filled and the message should show the amount left.
- Add more until you pass the threshold. The bar should fill and switch to the success message.
- Remove an item. The bar should shrink again.
- Test with a screen reader and with reduced motion turned on.
If you would rather not maintain this
Everything above works, but themes change, cart drawers differ and multi-currency adds edge cases. If you would rather not maintain it, Brix is the Shopify cart app I work on. It includes a multi-tier rewards progress bar, plus upsells, coupons and bundles, without custom code.
Questions about the Liquid or the script are welcome in the comments.
Top comments (0)