jelonyx
No-app fixes · Fix 02

Shopify free shipping bar
without an app.

Showing customers how close they are to free shipping lifts average order value. Most stores reach for a paid app. This fix does it with a single Liquid snippet. You get live progress tracking without a paid app or third-party script.

DifficultyBeginner
Time~15 minutes
CostFree
Theme accessRequired
Compatible with:Dawn 12+HorizonSenseCraftRefreshMost OS 2.0 themes
Preview workbench

Preview the shipping threshold.

Move the cart total and threshold to see the progress message merchants can install in the cart drawer or cart page.

Live storefront preview

The code

One file. Set your threshold on line 8, copy the rest.

snippets/jlx-fsbar.liquid
Liquid · CSS · JS
{% comment %}
  Jelonyx · Free Shipping Progress Bar : no app required
  Compatible with Dawn 12+, Horizon, and most Online Store 2.0 themes
  Source: jelonyx.com/shopify/no-app/free-shipping-bar
{% endcomment %}

{%- comment -%} Change this to your free shipping threshold in your store's currency (e.g. 50 for $50) {%- endcomment -%}
{%- assign jlx_threshold = 50 -%}
{%- assign jlx_threshold_cents = jlx_threshold | times: 100 -%}

<div
  id="jlx-fsbar"
  class="jlx-fsbar"
  data-threshold="{{ jlx_threshold_cents }}"
  data-total="{{ cart.total_price }}"
  data-currency="{{ cart.currency.iso_code }}"
  aria-live="polite"
  aria-atomic="true"
>
  <div
    class="jlx-fsbar__track"
    role="progressbar"
    aria-valuemin="0"
    aria-valuemax="{{ jlx_threshold_cents }}"
    aria-valuenow="{{ cart.total_price }}"
  >
    <div class="jlx-fsbar__fill"></div>
  </div>
  <p class="jlx-fsbar__msg"></p>
</div>

<style>
  .jlx-fsbar {
    padding: 12px 0;
    font-family: inherit;
  }
  .jlx-fsbar__track {
    height: 4px;
    background: var(--color-border, rgba(0, 0, 0, 0.1));
    border-radius: 99px;
    overflow: hidden;
    margin-bottom: 10px;
  }
  .jlx-fsbar__fill {
    height: 100%;
    background: var(--color-foreground, #1a1a1a);
    border-radius: 99px;
    transition: width 0.4s ease;
    width: 0%;
  }
  .jlx-fsbar__fill--complete {
    background: #2e7d32;
  }
  .jlx-fsbar__msg {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-foreground, #1a1a1a);
  }
  .jlx-fsbar__msg strong {
    font-weight: 600;
  }
</style>

<script>
  (function () {
    "use strict";

    var bar = document.getElementById("jlx-fsbar");
    if (!bar) return;

    var threshold = parseInt(bar.dataset.threshold, 10);
    var currency = bar.dataset.currency || "USD";
    var fill = bar.querySelector(".jlx-fsbar__fill");
    var msg = bar.querySelector(".jlx-fsbar__msg");
    var track = bar.querySelector(".jlx-fsbar__track");

    function formatMoney(cents) {
      try {
        return new Intl.NumberFormat(undefined, {
          style: "currency",
          currency: currency,
          minimumFractionDigits: 0,
          maximumFractionDigits: 2,
        }).format(cents / 100);
      } catch (e) {
        return (cents / 100).toFixed(2);
      }
    }

    function render(total) {
      var pct = Math.min((total / threshold) * 100, 100);
      fill.style.width = pct + "%";
      track.setAttribute("aria-valuenow", total);

      if (total >= threshold) {
        fill.classList.add("jlx-fsbar__fill--complete");
        msg.innerHTML = "<strong>You get free shipping!</strong>";
      } else {
        fill.classList.remove("jlx-fsbar__fill--complete");
        var remaining = threshold - total;
        msg.innerHTML =
          "Add <strong>" + formatMoney(remaining) + "</strong> more to get free shipping.";
      }
    }

    function fetchCart() {
      fetch("/cart.js")
        .then(function (r) { return r.json(); })
        .then(function (data) {
          currency = data.currency || currency;
          render(data.total_price);
        })
        .catch(function () {});
    }

    // Render initial state from Liquid-rendered data attribute (no flash)
    render(parseInt(bar.dataset.total, 10) || 0);

    // Re-render on cart events fired by Dawn and Horizon
    document.addEventListener("cart:updated", fetchCart);
    document.addEventListener("cart:refresh", fetchCart);

    // Intercept fetch calls that mutate the cart and re-fetch totals
    var _fetch = window.fetch;
    window.fetch = function () {
      var result = _fetch.apply(this, arguments);
      var input = arguments[0];
      var url =
        typeof input === "string"
          ? input
          : input instanceof Request
          ? input.url
          : "";
      if (//cart/(add|change|update)(.js)?/.test(url)) {
        result.then(function (r) {
          if (r.ok) setTimeout(fetchCart, 100);
          return r;
        }).catch(function () {});
      }
      return result;
    };
  })();
</script>

How to install

  1. 01
    Open your theme code editor.

    In Shopify Admin, go to Online Store → Themes. On your active theme, click the three-dot menu and select Edit code.

  2. 02
    Create a new snippet.

    Under the Snippets folder, click Add a new snippet. Name it jlx-fsbar and click Done.

  3. 03
    Paste the full snippet code.

    Delete any placeholder content and paste the entire code block above. Save the file.

  4. 04
    Set your free shipping threshold.

    On line 8 of the snippet, change the number 50 to your store's actual free shipping threshold in your store's currency (e.g. 75 for $75, 100 for £100). Save again.

  5. 05
    Add the render tag to your cart drawer.

    Open sections/main-cart-drawer.liquid. Find the <ul> tag that lists cart items (it will have an id like cart-drawer-items). Add the following on the line directly above it, then save.

    {% render 'jlx-fsbar' %}

    To also show the bar on the cart page, repeat this step in sections/main-cart.liquid, placing the render tag above the cart items table.

  6. 06
    Test in the cart.

    Add a product to your cart and open the cart drawer. The progress bar should appear showing how much more the customer needs for free shipping. Add another item to see the bar update live without a page reload.

How it works

The snippet renders with the current cart total baked into a data-total attribute by Liquid, so the bar displays the correct state immediately on load without a flash of empty content.

After that, the JavaScript keeps the bar in sync three ways: it listens for cart:updated and cart:refresh events (which Dawn and Horizon fire when the cart changes), and it wraps window.fetch to detect calls to /cart/add.js, /cart/change.js, and /cart/update.js. It then re-fetches /cart.js to get the new total.

Currency formatting uses the browser's built-in Intl.NumberFormat with the ISO currency code from the cart API response, so the amount displays correctly for every currency Shopify supports, with no hardcoded symbols.

The progress bar fill and message text both update in a single render call, and the underlying progressbar role is kept in sync via aria-valuenow for screen readers.

Compatibility

Tested against Dawn 12+ and Horizon. Both themes use CSS variables (--color-border, --color-foreground) that the snippet reads for the track and fill colours. The bar matches your store without any CSS edits.

On themes that do not define these variables, the snippet falls back to neutral values (light grey track, dark fill). You can override the .jlx-fsbar__track and .jlx-fsbar__fill classes in the snippet CSS to hard-code your colours.

The fetch interceptor approach covers themes that use standard Shopify AJAX cart calls. Themes using custom WebSocket or server-sent-event cart updates may not trigger the bar refresh automatically. In that case, the cart:updated event listener will still catch refreshes if the theme fires it.

Limitations

  • Threshold is set in code: the snippet does not read from theme settings. You set the number directly in line 8. If you change your free shipping threshold, update the snippet.
  • Discount codes: Shopify's /cart.js returns the cart total before discount codes are applied at checkout. If your store uses cart-level discount codes that reduce the total, the bar may show a higher remaining amount than the actual amount needed.
  • Subscription items: if your store uses subscription apps that manage their own cart totals, the bar may not reflect subscription discounts correctly.
  • Headless storefronts: this is a Liquid snippet and does not apply to Hydrogen or other headless setups.
No-App Shopify Fix Sprint

Need this installed?

If you would rather not touch theme code, we can install, configure, and style this for your store. We will set the correct threshold, match your brand colours, and test it against your specific cart setup.