Free Shipping Progress Bar

"You're £12 away from free shipping" with a bar that fills as the cart grows.

One section file Header announcement area, cart page or product page Edit in the theme editor

Important: Before making any changes to your theme, duplicate it in Online Store > Themes so you have a backup.

A free shipping bar turns your shipping threshold into a goal. Shoppers who are close add one more item instead of paying for delivery. This section reads the cart, shows how much is left to spend, and updates when items are added or removed.

Preview

What you'll build

You're £12.00 away from free shipping

You've unlocked free shipping!

Preview with the default settings. Colours, text and layout can all be changed in the theme editor.

Install

Add it to your store in 4 steps

  1. 1

    Open your theme code

    In Shopify admin go to Online Store > Themes. Next to the theme you want to edit, click the three dots and choose Edit code.

  2. 2

    Create the section

    In the Sections folder click Add a new section, name it lt-free-shipping-bar, delete the placeholder code, paste in the code below and click Save.

  3. 3

    Add it in the theme editor

    Click Customize, go to the header area of any page (or the Cart template), click Add section and choose Free shipping bar.

  4. 4

    Make it yours

    Change the settings in the sidebar, drag the section where you want it, and click Save.

sections/lt-free-shipping-bar.liquid · 185 lines
{%- comment -%}
  Free shipping progress bar by LAUNCHTIP
  https://launchtip.com/free-shopify-sections-code
{%- endcomment -%}
{%- liquid
  assign threshold = section.settings.threshold | times: 100
  assign remaining = threshold | minus: cart.total_price
  assign remaining_money = remaining | money
  assign percent = 100
  if threshold > 0
    assign percent = cart.total_price | times: 100 | divided_by: threshold
  endif
  if percent > 100
    assign percent = 100
  endif
-%}

{%- style -%}
  #lt-{{ section.id }} { background: {{ section.settings.background }}; color: {{ section.settings.text_color }}; padding: {{ section.settings.padding }}px 16px; text-align: center; }
  #lt-{{ section.id }}[hidden] { display: none; }
  #lt-{{ section.id }} .lt-fsb__message { margin: 0 0 8px; font-size: 0.95em; }
  #lt-{{ section.id }} .lt-fsb__track { max-width: 420px; height: 8px; margin: 0 auto; border-radius: 999px; background: {{ section.settings.track_color }}; overflow: hidden; }
  #lt-{{ section.id }} .lt-fsb__fill { height: 100%; width: 0; border-radius: 999px; background: {{ section.settings.bar_color }}; transition: width 0.4s ease; }
{%- endstyle -%}

<div
  id="lt-{{ section.id }}"
  data-threshold="{{ threshold }}"
  data-total="{{ cart.total_price }}"
  data-count="{{ cart.item_count }}"
  data-currency="{{ cart.currency.iso_code }}"
  data-cart-url="{{ routes.cart_url }}"
  data-hide-empty="{{ section.settings.hide_when_empty }}"
  data-message-below="{{ section.settings.message_below | escape }}"
  data-message-reached="{{ section.settings.message_reached | escape }}"
  {% if section.settings.hide_when_empty and cart.item_count == 0 %}hidden{% endif %}
>
  <p class="lt-fsb__message" aria-live="polite">
    {%- if remaining > 0 -%}
      {{ section.settings.message_below | replace: '[remaining]', remaining_money }}
    {%- else -%}
      {{ section.settings.message_reached }}
    {%- endif -%}
  </p>
  <div class="lt-fsb__track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ percent }}">
    <div class="lt-fsb__fill" style="width: {{ percent }}%;"></div>
  </div>
</div>

<script>
  (function () {
    var root = document.getElementById('lt-{{ section.id }}');
    if (!root || root.dataset.ready) return;
    root.dataset.ready = '1';

    var message = root.querySelector('.lt-fsb__message');
    var track = root.querySelector('.lt-fsb__track');
    var fill = root.querySelector('.lt-fsb__fill');

    function rate() {
      var r = window.Shopify && Shopify.currency && Number(Shopify.currency.rate);
      return r > 0 ? r : 1;
    }

    function money(cents) {
      var currency = (window.Shopify && Shopify.currency && Shopify.currency.active) || root.dataset.currency;
      return new Intl.NumberFormat(document.documentElement.lang || undefined, { style: 'currency', currency: currency }).format(cents / 100);
    }

    function render(total, count) {
      var threshold = Math.round(Number(root.dataset.threshold) * rate());
      var remaining = Math.max(threshold - total, 0);
      var percent = threshold > 0 ? Math.min(100, Math.round(total / threshold * 100)) : 100;
      root.hidden = root.dataset.hideEmpty === 'true' && count === 0;
      message.textContent = remaining > 0
        ? root.dataset.messageBelow.replace('[remaining]', money(remaining))
        : root.dataset.messageReached;
      fill.style.width = percent + '%';
      track.setAttribute('aria-valuenow', percent);
    }

    function refresh() {
      fetch(root.dataset.cartUrl + '.js', { headers: { Accept: 'application/json' } })
        .then(function (r) { return r.json(); })
        .then(function (cart) { render(cart.total_price, cart.item_count); })
        .catch(function () {});
    }

    // Themes don't share one cart event, so watch the cart endpoints instead.
    if (!window.__ltCartWatch) {
      window.__ltCartWatch = true;
      var originalFetch = window.fetch;
      window.fetch = function (resource) {
        var request = originalFetch.apply(this, arguments);
        var url = typeof resource === 'string' ? resource : (resource && resource.url) || '';
        if (/\/cart\/(add|change|update|clear)/.test(url)) {
          request.then(function () { document.dispatchEvent(new CustomEvent('lt:cart-changed')); });
        }
        return request;
      };
    }

    ['lt:cart-changed', 'cart:refresh', 'cart:updated', 'cart:change'].forEach(function (name) {
      document.addEventListener(name, refresh);
    });

    render(Number(root.dataset.total), Number(root.dataset.count));
    if (rate() !== 1) refresh();
  })();
</script>

{% schema %}
{
  "name": "Free shipping bar",
  "tag": "section",
  "class": "lt-free-shipping-section",
  "settings": [
    {
      "type": "number",
      "id": "threshold",
      "label": "Free shipping threshold",
      "default": 50,
      "info": "In your store's main currency, with no symbol. Match this to your free shipping rate in Settings > Shipping and delivery."
    },
    {
      "type": "text",
      "id": "message_below",
      "label": "Message below threshold",
      "default": "You're [remaining] away from free shipping",
      "info": "[remaining] is replaced with the amount left to spend."
    },
    {
      "type": "text",
      "id": "message_reached",
      "label": "Message when reached",
      "default": "You've unlocked free shipping!"
    },
    {
      "type": "checkbox",
      "id": "hide_when_empty",
      "label": "Hide when the cart is empty",
      "default": false
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background",
      "default": "#f5f5f5"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text colour",
      "default": "#111111"
    },
    {
      "type": "color",
      "id": "bar_color",
      "label": "Bar colour",
      "default": "#16a34a"
    },
    {
      "type": "color",
      "id": "track_color",
      "label": "Track colour",
      "default": "#e5e5e5"
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Vertical padding",
      "min": 4,
      "max": 40,
      "step": 2,
      "unit": "px",
      "default": 12
    }
  ],
  "presets": [
    {
      "name": "Free shipping bar"
    }
  ]
}
{% endschema %}
Customization

What you can change in the theme editor

Setting Default
Free shipping threshold In your store's main currency, with no symbol. Match this to your free shipping rate in Settings > Shipping and delivery. 50
Message below threshold [remaining] is replaced with the amount left to spend. You're [remaining] away from free shipping
Message when reached You've unlocked free shipping!
Hide when the cart is empty Off
Background #f5f5f5
Text colour #111111
Bar colour #16a34a
Track colour #e5e5e5
Vertical padding 12px
Good to know

Tips for setting it up

  • Set the threshold to the same amount as your free shipping rate in Settings > Shipping and delivery.
  • The threshold is in your store's main currency. When a shopper browses in another currency the bar converts it using Shopify's exchange rate.
  • Add it to the header group to show it on every page, or to the cart template to show it only where it matters most.

Further reading: Shopify AOV calculator

FAQ

Frequently asked questions

Does it update without a page refresh?

Yes. It watches the cart requests your theme makes and re-reads the cart after each one, so it updates when items are added, removed or changed.

Can I hide it when the cart is empty?

Yes, turn on Hide when the cart is empty.

Does it work in the cart drawer?

It shows wherever you place the section. Cart drawers are built into each theme, so to show it inside the drawer you'd need to add the markup to your theme's drawer file.

SellUp

Want this without the code?

A progress bar tells shoppers to add more. SellUp shows them what to add, with cart upsells and add-ons matched to what's already in the basket.

Try SellUp

More free sections

New

Gift Wrap Checkbox

A paid gift wrap add-on with an optional gift message, right above add to cart.

New

Frequently Bought Together

Hand-picked product bundles with checkboxes, a running total and one add-all button.

New

Quantity Price-Break Table

Buy 2 save 10%, buy 3 save 15%: a tiered pricing table with the price each worked out.

Explore More Free Sections

Discover more code snippets to enhance your Shopify store.

View All Sections
SellUp Increase AOV with Upsells