Free Shipping Progress Bar Section for ShopifyNo app. Copy, paste, done.
- Online Store 2.0
- No app
- Theme Check: 0 errors
- MIT license
The Stackframes shipping progress bar is a free Shopify section that tells shoppers how much more they need to spend to get free shipping, with a progress bar. When they reach the threshold it switches to a success message. It reads the cart in Liquid and refreshes itself when items are added or changed. It hides automatically for shoppers browsing in a different currency, so it never shows a wrong amount.
Live preview with sample content. Once installed, fonts follow your theme and colours are editable in the theme editor.
{%- liquid
assign threshold_cents = section.settings.threshold | times: 100 | round
assign same_currency = false
if cart.currency.iso_code == shop.currency
assign same_currency = true
endif
assign show = false
if threshold_cents > 0 and same_currency
assign show = true
endif
assign remaining = 0
assign percent = 0
if show
assign remaining = threshold_cents | minus: cart.total_price
if remaining < 0
assign remaining = 0
endif
assign percent = cart.total_price | times: 100 | divided_by: threshold_cents | floor
if percent > 100
assign percent = 100
endif
endif
assign amount = remaining | money
assign progress_message = section.settings.progress_message | escape | replace: '[amount]', amount
-%}
{%- if show or request.design_mode -%}
<sf-shipping-progress
class="sf-ship"
data-threshold="{{ threshold_cents }}"
data-currency="{{ shop.currency }}"
data-money-format="{{ shop.money_format | escape }}"
data-progress="{{ section.settings.progress_message | escape }}"
data-success="{{ section.settings.success_message | escape }}"
style="--sf-ship-bg: {{ section.settings.background_color }}; --sf-ship-text: {{ section.settings.text_color }}; --sf-ship-fill: {{ section.settings.bar_color }}; --sf-ship-py: {{ section.settings.padding }}px;"
>
{%- if show -%}
<p class="sf-ship__message" aria-live="polite">
{%- if remaining > 0 -%}{{ progress_message }}{%- else -%}{{ section.settings.success_message | escape }}{%- endif -%}
</p>
{%- if section.settings.show_bar -%}
<div class="sf-ship__bar" role="progressbar" aria-label="Progress to free shipping" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ percent }}">
<span class="sf-ship__fill" style="width: {{ percent }}%"></span>
</div>
{%- endif -%}
{%- elsif threshold_cents <= 0 -%}
<p class="sf-ship__empty">Set a free shipping threshold in the section settings to show this bar.</p>
{%- else -%}
<p class="sf-ship__empty">This bar is hidden when shoppers browse in another currency, because amounts can't be converted in Liquid. It shows for carts in {{ shop.currency }}.</p>
{%- endif -%}
</sf-shipping-progress>
{%- endif -%}
{% stylesheet %}
.sf-ship { display: block; background: var(--sf-ship-bg); color: var(--sf-ship-text); padding: var(--sf-ship-py) 16px; text-align: center; }
.sf-ship__message { margin: 0 auto; max-width: 720px; font-weight: 600; }
.sf-ship__bar { position: relative; overflow: hidden; max-width: 480px; height: 8px; margin: 8px auto 0; border-radius: 999px; background: color-mix(in srgb, currentColor 18%, transparent); }
.sf-ship__fill { display: block; height: 100%; border-radius: inherit; background: var(--sf-ship-fill); transition: width 0.4s ease; }
.sf-ship__empty { margin: 0 auto; max-width: 640px; padding: 12px 16px; border: 1px dashed currentColor; border-radius: 8px; font-size: 0.875em; opacity: 0.8; }
@media (prefers-reduced-motion: reduce) { .sf-ship__fill { transition: none; } }
{% endstylesheet %}
{% javascript %}
if (!customElements.get('sf-shipping-progress')) {
const CART_WRITES = ['/cart/add', '/cart/change', '/cart/update', '/cart/clear'];
const CART_EVENTS = ['cart:updated', 'cart:refresh', 'cart:change'];
class SfShippingProgress extends HTMLElement {
static watchCart() {
if (window.__sfCartWatch || typeof window.fetch !== 'function') return;
window.__sfCartWatch = true;
const originalFetch = window.fetch;
window.fetch = function (...args) {
const result = originalFetch.apply(window, args);
const target = args[0];
const url = String(target && target.url ? target.url : target);
if (CART_WRITES.some((path) => url.includes(path))) {
result
.then(() => document.querySelectorAll('sf-shipping-progress').forEach((el) => el.refresh()))
.catch(() => {});
}
return result;
};
}
connectedCallback() {
SfShippingProgress.watchCart();
this.onCartEvent = () => this.refresh();
CART_EVENTS.forEach((type) => document.addEventListener(type, this.onCartEvent));
}
disconnectedCallback() {
CART_EVENTS.forEach((type) => document.removeEventListener(type, this.onCartEvent));
}
async refresh() {
try {
const root = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
const response = await fetch(root + 'cart.js', { headers: { Accept: 'application/json' } });
if (!response.ok) return;
this.update(await response.json());
} catch (error) {
// Network errors leave the last rendered state in place.
}
}
update(cart) {
const threshold = Number(this.dataset.threshold);
if (!threshold || !cart || cart.currency !== this.dataset.currency) return;
const remaining = Math.max(0, threshold - cart.total_price);
const percent = Math.min(100, Math.floor((cart.total_price * 100) / threshold));
const message = this.querySelector('.sf-ship__message');
if (message) {
message.textContent = remaining > 0
? this.dataset.progress.split('[amount]').join(this.format(remaining))
: this.dataset.success;
}
const bar = this.querySelector('.sf-ship__bar');
const fill = this.querySelector('.sf-ship__fill');
if (bar) bar.setAttribute('aria-valuenow', String(percent));
if (fill) fill.style.width = percent + '%';
}
format(cents) {
const pattern = this.dataset.moneyFormat || '';
const match = pattern.match(new RegExp('\\u007b\\u007b\\s*(\\w+)\\s*\\u007d\\u007d'));
if (match) {
const group = (n, sep) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, sep);
const value = cents / 100;
const whole = group(Math.round(value), match[1].includes('comma') ? '.' : ',');
const fixed = Math.abs(value).toFixed(2);
let amount = null;
if (match[1] === 'amount') amount = group(fixed.split('.')[0], ',') + '.' + fixed.split('.')[1];
else if (match[1] === 'amount_no_decimals') amount = whole;
else if (match[1] === 'amount_with_comma_separator') amount = group(fixed.split('.')[0], '.') + ',' + fixed.split('.')[1];
else if (match[1] === 'amount_no_decimals_with_comma_separator') amount = whole;
if (amount !== null) return pattern.replace(match[0], amount).replace(/<[^>]*>/g, '');
}
try {
return new Intl.NumberFormat(document.documentElement.lang || undefined, {
style: 'currency',
currency: this.dataset.currency,
}).format(cents / 100);
} catch (error) {
return (cents / 100).toFixed(2);
}
}
}
customElements.define('sf-shipping-progress', SfShippingProgress);
}
{% endjavascript %}
{% schema %}
{
"name": "SF free shipping bar",
"tag": "section",
"class": "sf-ship-section",
"settings": [
{ "type": "number", "id": "threshold", "label": "Free shipping threshold", "info": "In your store currency, e.g. 50. Use the same amount as your shipping rate rules.", "default": 50 },
{ "type": "text", "id": "progress_message", "label": "Progress message", "info": "Use [amount] where the remaining amount should appear.", "default": "You're [amount] away from free shipping" },
{ "type": "text", "id": "success_message", "label": "Success message", "default": "You've unlocked free shipping!" },
{ "type": "checkbox", "id": "show_bar", "label": "Show progress bar", "default": true },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "background_color", "label": "Background", "default": "#f3f4f6" },
{ "type": "color", "id": "text_color", "label": "Text", "default": "#111827" },
{ "type": "color", "id": "bar_color", "label": "Bar", "default": "#16a34a" },
{ "type": "range", "id": "padding", "label": "Vertical padding", "min": 4, "max": 32, "step": 2, "unit": "px", "default": 12 }
],
"presets": [{ "name": "SF free shipping bar" }]
}
{% endschema %}
- Remaining amount formatted in your store's currency
- Refreshes when shoppers add or change cart items
- Hidden for other currencies instead of showing wrong numbers
- Accessible progress bar with live message updates
Add the Shipping Progress Bar to your theme
About two minutes. No app, no monthly fee.
Prefer a walkthrough? See the full installation guide.
- STEP 01
Duplicate your theme
In Shopify admin go to Online Store → Themes. Next to your current theme click ⋯ → Duplicate. Make every change on the copy.
- STEP 02
Create the section file
On the copy, click ⋯ → Edit code. In the sections folder click “Add a new section”, choose Liquid, name it sf-free-shipping-progress-bar and create it. Delete the starter code, paste ours, then click Save.
- STEP 03
Add it to a page
Click Customize, open the template you want, click “Add section” and choose “SF free shipping bar”. Edit the text, blocks and colours in the sidebar, then Save.
- STEP 04
Check it, then publish
Preview on mobile and desktop. When you are happy, publish the duplicate theme.
What you can change in the theme editor
| Setting | Type | Default |
|---|---|---|
| Free shipping thresholdIn your store currency, e.g. 50. Use the same amount as your shipping rate rules. | Number | 50 |
| Progress messageUse [amount] where the remaining amount should appear. | Text | You're [amount] away from free shipping |
| Success message | Text | You've unlocked free shipping! |
| Show progress bar | Checkbox | On |
| Background | Colour | #f3f4f6 |
| Text | Colour | #111827 |
| Bar | Colour | #16a34a |
| Vertical padding | Slider | 12px |
This section is one piece of a custom theme.
We design and build complete Shopify themes around your brand.
Common questions
Does it actually change my shipping rates?
No. It only displays progress. Set up the free shipping rate itself under Settings → Shipping and delivery, using the same threshold.
Why does the bar disappear for some shoppers?
Liquid can't convert currencies, so when a shopper uses a different currency through Shopify Markets the bar hides rather than show an incorrect amount.
Will it update in my cart drawer?
It refreshes when your theme updates the cart with Shopify's cart API over fetch (most Online Store 2.0 themes, including Dawn) and when common cart events fire. Older themes that use jQuery or XHR need a small tweak — place it on the cart page, or ask us to wire it into your cart drawer.
More free sections
Countdown Timer Bar
Free countdown timer bar for Shopify sales that ends at a real date and time you set, then hides or shows a message. Liquid code, no app.
Announcement Marquee
Free scrolling announcement bar for Shopify with multiple messages, links, a pause button and reduced-motion support. Paste the Liquid, no app.
Trust Badges
Free trust badge section for Shopify: shipping, returns, secure checkout and support icons. Lightweight inline SVG, no JavaScript, no app.
All free Shopify sections
Get a Shopify theme built around your brand
Custom sections, a full theme build, or changes to the theme you already have. Tell us what your store needs and we'll scope it with you.
Not affiliated with or endorsed by Shopify Inc. Shopify is a trademark of Shopify Inc.