Shopify Free Shipping Bar Generator

Show shoppers how close they are to free shipping, with a progress bar that updates as the cart changes and handles other currencies.

Describe the result you need. AI will set up the tool and ask for any missing details.

Configure with AI

Enter to submit · Shift + Enter for a new line

Sent to AI. Leave out passwords and private customer data.

Your details

Threshold

In your store currency. Use the same amount as your free shipping rate.

Settings → General → Store currency.

Only for markets where you set a local free shipping rate. Other currencies are converted automatically.

Messages

[amount] becomes the amount left to spend.

[threshold] becomes the threshold.

Colors

Your section file (sections/custom-free-shipping-bar.liquid)

custom-free-shipping-bar.liquid
{%- comment -%}
  Free shipping progress bar. Reads cart.total_price (after discounts, in the shopper's currency, in cents)
  and updates itself when the cart changes. Add it to the Header group or to your cart page.
{%- endcomment -%}
{%- liquid
  assign threshold_cents = 0
  if section.settings.threshold != blank
    assign threshold_cents = section.settings.threshold | times: 100 | round
  endif
  assign cart_currency = cart.currency.iso_code
  assign threshold_known = false
  if cart_currency == shop.currency
    assign threshold_known = true
  endif
  if section.settings.local_thresholds != blank
    assign pairs = section.settings.local_thresholds | remove: ' ' | upcase | split: ','
    for pair in pairs
      assign parts = pair | split: ':'
      if parts[0] == cart_currency and parts[1] != blank
        assign threshold_cents = parts[1] | times: 100 | round
        assign threshold_known = true
      endif
    endfor
  endif
  if threshold_cents <= 0
    assign threshold_known = false
  endif

  assign total = cart.total_price
  assign remaining = threshold_cents | minus: total
  assign remaining_money = remaining | money
  assign threshold_money = threshold_cents | money
  assign percent = 0
  if threshold_cents > 0
    assign percent = total | times: 100 | divided_by: threshold_cents
    if percent > 100
      assign percent = 100
    endif
  endif

  assign hide_bar = false
  if cart.item_count == 0 and section.settings.show_when_empty == false
    assign hide_bar = true
  endif
-%}

{%- style -%}
  #FreeShippingBar-{{ section.id }}{display:block;background:{{ section.settings.background }};color:{{ section.settings.text_color }};padding:.75rem 1rem}
  #FreeShippingBar-{{ section.id }}[hidden]{display:none}
  #FreeShippingBar-{{ section.id }} .ea-fsb__inner{max-width:640px;margin:0 auto}
  #FreeShippingBar-{{ section.id }} .ea-fsb__text{margin:0 0 .5rem;text-align:center;font-size:.95em}
  #FreeShippingBar-{{ section.id }} .ea-fsb__track{height:8px;border-radius:999px;background:{{ section.settings.track_color }};overflow:hidden}
  #FreeShippingBar-{{ section.id }} .ea-fsb__fill{display:block;height:100%;width:0;border-radius:inherit;background:{{ section.settings.bar_color }};transition:width .4s ease}
  @media (prefers-reduced-motion:reduce){#FreeShippingBar-{{ section.id }} .ea-fsb__fill{transition:none}}
{%- endstyle -%}

<ea-free-shipping-bar
  id="FreeShippingBar-{{ section.id }}"
  class="ea-fsb"
  data-threshold="{{ section.settings.threshold | default: 0 }}"
  data-local="{{ section.settings.local_thresholds | escape }}"
  data-shop-currency="{{ shop.currency }}"
  data-currency="{{ cart_currency }}"
  data-total="{{ cart.total_price }}"
  data-count="{{ cart.item_count }}"
  data-known="{{ threshold_known }}"
  data-show-empty="{{ section.settings.show_when_empty }}"
  data-message="{{ section.settings.message | escape }}"
  data-success="{{ section.settings.success_message | escape }}"
  data-empty="{{ section.settings.empty_message | escape }}"
  {% if hide_bar %}hidden{% endif %}
>
  <div class="ea-fsb__inner">
    <p class="ea-fsb__text" aria-live="polite">
      {%- if threshold_known -%}
        {%- if cart.item_count == 0 -%}
          {{- section.settings.empty_message | escape | replace: '[threshold]', threshold_money -}}
        {%- elsif remaining > 0 -%}
          {{- section.settings.message | escape | replace: '[amount]', remaining_money -}}
        {%- else -%}
          {{- section.settings.success_message | escape -}}
        {%- endif -%}
      {%- endif -%}
    </p>
    <div class="ea-fsb__track" role="progressbar" aria-label="Progress to free shipping" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ percent }}">
      <span class="ea-fsb__fill" style="width: {{ percent }}%;"></span>
    </div>
  </div>
</ea-free-shipping-bar>

<script>
  if (!customElements.get('ea-free-shipping-bar')) {
    customElements.define('ea-free-shipping-bar', class extends HTMLElement {
      connectedCallback() {
        if (this.dataset.known !== 'true') {
          this.render({ total_price: Number(this.dataset.total), currency: this.dataset.currency, item_count: Number(this.dataset.count) });
        }
        // Refresh whenever the theme adds, changes or clears cart items (fetch or XHR).
        if ('PerformanceObserver' in window && !this.observer) {
          this.observer = new PerformanceObserver((list) => {
            if (list.getEntries().some((e) => /\/cart\/(add|change|update|clear)/.test(e.name))) this.refresh();
          });
          try { this.observer.observe({ type: 'resource' }); } catch (e) {}
        }
        this.onShow = () => this.refresh();
        window.addEventListener('pageshow', this.onShow);
      }
      disconnectedCallback() {
        if (this.observer) this.observer.disconnect();
        this.observer = null;
        window.removeEventListener('pageshow', this.onShow);
      }
      refresh() {
        clearTimeout(this.wait);
        this.wait = setTimeout(() => {
          const root = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
          fetch(root + 'cart.js', { headers: { Accept: 'application/json' } })
            .then((r) => r.json())
            .then((cart) => this.render(cart))
            .catch(() => {});
        }, 150);
      }
      thresholdFor(currency) {
        const pairs = (this.dataset.local || '').replace(/\s/g, '').toUpperCase().split(',');
        for (const pair of pairs) {
          const [code, amount] = pair.split(':');
          if (code === currency && amount && !isNaN(parseFloat(amount))) return Math.round(parseFloat(amount) * 100);
        }
        const base = parseFloat(this.dataset.threshold) * 100;
        if (!base) return null;
        if (currency === this.dataset.shopCurrency) return Math.round(base);
        // Shopify Markets: convert the store-currency threshold with the storefront's live rate.
        const rate = window.Shopify && window.Shopify.currency && parseFloat(window.Shopify.currency.rate);
        return rate ? Math.round(base * rate) : null;
      }
      format(cents, currency) {
        try {
          return new Intl.NumberFormat(document.documentElement.lang || undefined, { style: 'currency', currency }).format(cents / 100);
        } catch (e) {
          return (cents / 100).toFixed(2) + ' ' + currency;
        }
      }
      render(cart) {
        const threshold = this.thresholdFor(cart.currency);
        if (!threshold) { this.hidden = true; return; }
        const total = Number(cart.total_price) || 0;
        const remaining = threshold - total;
        const pct = Math.max(0, Math.min(100, Math.floor((total / threshold) * 100)));
        let text;
        if (!cart.item_count) {
          if (this.dataset.showEmpty !== 'true') { this.hidden = true; return; }
          text = this.dataset.empty.replace('[threshold]', this.format(threshold, cart.currency));
        } else if (remaining > 0) {
          text = this.dataset.message.replace('[amount]', this.format(remaining, cart.currency));
        } else {
          text = this.dataset.success;
        }
        this.hidden = false;
        this.querySelector('.ea-fsb__text').textContent = text;
        this.querySelector('.ea-fsb__fill').style.width = pct + '%';
        this.querySelector('[role="progressbar"]').setAttribute('aria-valuenow', String(pct));
      }
    });
  }
</script>

{% schema %}
{
  "name": "Free shipping bar",
  "tag": "section",
  "class": "ea-free-shipping-bar-section",
  "settings": [
    {
      "type": "number",
      "id": "threshold",
      "label": "Free shipping threshold",
      "info": "In your store currency (USD). Match the minimum on your free shipping rate.",
      "default": 75
    },
    {
      "type": "text",
      "id": "local_thresholds",
      "label": "Fixed thresholds in other currencies",
      "info": "Optional. For markets with their own free shipping rate, for example EUR:70, GBP:60. Other currencies are converted automatically."
    },
    {
      "type": "text",
      "id": "message",
      "label": "Message below the threshold",
      "info": "[amount] is replaced with the amount left to spend.",
      "default": "You're [amount] away from free shipping"
    },
    {
      "type": "text",
      "id": "success_message",
      "label": "Message once reached",
      "default": "You've unlocked free shipping!"
    },
    {
      "type": "checkbox",
      "id": "show_when_empty",
      "label": "Show when the cart is empty",
      "default": true
    },
    {
      "type": "text",
      "id": "empty_message",
      "label": "Empty cart message",
      "info": "[threshold] is replaced with the threshold.",
      "default": "Free shipping on orders over [threshold]"
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background",
      "default": "#f4f1ea"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text",
      "default": "#1a1a1a"
    },
    {
      "type": "color",
      "id": "bar_color",
      "label": "Progress",
      "default": "#2e7d4f"
    },
    {
      "type": "color",
      "id": "track_color",
      "label": "Track",
      "default": "#dcd6c8"
    }
  ],
  "presets": [
    {
      "name": "Free shipping bar"
    }
  ]
}
{% endschema %}

To install: in Shopify admin go to Online Store → Themes, click … next to your theme → Edit code. Under Sections choose Add a new section, name it as shown above, replace the contents with this code and save. Then open Customize and add the section where you want it. Try it on a duplicate of your theme first. Put it in the Header group to show it on every page, or on the cart page template.

Other currencies: shoppers paying in USD see the threshold as entered. Any currency listed under fixed thresholds uses that amount. Every other currency is converted with the storefront's exchange rate (Shopify.currency.rate), so the bar can be a few cents off the converted threshold Shopify uses at checkout. For an exact match, set a local free shipping rate for that market and list it here.

Live preview

How the bar works out what's left to spend

Shopify gives the cart total as cart.total_price: the price after discounts, in the shopper's currency, in cents. The bar compares it with your threshold and fills in the gap.

Amount left = threshold − cart total · Progress = cart total ÷ threshold

The generator's default

Free shipping threshold
$75.00
Cart total
$48.00

The bar says You're $27.00 away from free shipping and is 64% full (48 ÷ 75).

When a shopper adds, changes or removes an item, the bar notices the theme's cart request and reloads /cart.js, so it stays right in cart drawers and on the cart page without a refresh.

Other currencies with Shopify Markets

With Markets, a shopper in Germany sees the cart in euros, so a $75 threshold has to be shown in euros too. The bar picks the threshold in this order:

  • Your store currency: the threshold exactly as you entered it.
  • Currencies you list under fixed thresholds, such as EUR:70: that amount. Use this for markets where you set a local free shipping rate, so the bar matches checkout exactly.
  • Any other currency: your threshold converted with the storefront's exchange rate (Shopify.currency.rate). If the rate were 0.92, $75 would show as €69.00.

Shopify converts prices with the exchange rate plus a conversion fee, then applies rounding if you've turned it on. The converted threshold on the bar can therefore be a few cents off what checkout uses. If that matters, set a local free shipping rate for the market and list it as a fixed threshold.

Adding it to your store

  1. 01

    Check your rate

    In Settings → Shipping and delivery, open your shipping profile and make sure your free shipping rate uses the same minimum as the bar.

  2. 02

    Add the section

    In Online Store → Themes → … → Edit code, add a new section named custom-free-shipping-bar and paste the code.

  3. 03

    Place it

    In Customize, add Free shipping bar to the Header group to show it on every page, or to your cart page template.

Not sure where to set the threshold? The free shipping threshold calculator finds the break-even point from your order value and margin.

Questions people ask

Does the bar update without reloading the page?

Yes. It watches for the theme's add, change, update and clear cart requests, then reloads the cart and redraws itself.

Does it count discounts?

Yes. cart.total_price is the total after discounts, so a discount code that drops the cart below the threshold shows the shopper how much they still need.

Why is the converted amount slightly different from checkout?

Shopify adds a conversion fee and can round converted prices, and the storefront rate doesn't reflect every rule. List a fixed local threshold for markets where you need an exact match.

Can I put it inside my cart drawer?

Theme cart drawers are snippets, so the simplest option is the Header group, which is visible while the drawer is open. Placing it inside a drawer needs a theme edit specific to your theme.

Want this builtinto your store?

I'm Emmanuel, a Shopify and WooCommerce developer. If this tool pointed at something you'd rather have fixed for you (a faster store, cleaner product data, a migration, a custom feature), send me a note.

Emmanuel Asika

Emmanuel Asika

Shopify & WooCommerce developer, Dublin

I build and fix online stores: new builds, migrations, speed, SEO and the custom bits apps can't do.

hello@emmanuelasika.com