E-commerce & Retail

Free Free Shipping Progress Widget

Show shoppers exactly how close they are to unlocking free shipping.

A lightweight progress bar that tells customers how much more they need to spend to earn free shipping, with a celebratory success state once the goal is reached. Built for e-commerce stores on any platform. Drop in one script tag, set your goal and cart amount, and it renders right where you paste it.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/free-shipping-progress.js" data-hl-current="38" async></script>
<!-- Free Free Shipping Progress widget by haynes lab — https://hayneslab.dev/tools/widgets/free-shipping-progress -->
Inline (self-contained)html
<!-- Free Free Shipping Progress widget by haynes lab — https://hayneslab.dev/tools/widgets/free-shipping-progress -->
<script data-hl-current="38">
/*!
 * haynes lab — Free Shipping Progress widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/free-shipping-progress
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-free-shipping-progress';
  var STYLE_ID = 'hlw-free-shipping-progress-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/free-shipping-progress.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 480px; margin: 0 auto; padding: 20px 24px 26px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 15px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__message { display: flex; align-items: center; gap: 8px; margin: 0 0 12px;' +
      ' font-size: 15px; color: #374151; }' +
      '.' + PREFIX + '__message strong { color: #111827; font-weight: 600; }' +
      '.' + PREFIX + '__check { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;' +
      ' width: 22px; height: 22px; border-radius: 50%; background: var(--hlw-accent); color: #ffffff;' +
      ' font-size: 13px; line-height: 1; }' +
      '.' + PREFIX + '__track { height: 10px; border-radius: 999px; background: #e5e7eb; overflow: hidden; }' +
      '.' + PREFIX + '__fill { height: 100%; width: 0; border-radius: 999px; background: var(--hlw-accent);' +
      ' transition: width 0.6s ease; }' +
      '.' + PREFIX + '__meta { display: flex; justify-content: space-between; margin-top: 8px;' +
      ' font-size: 12px; color: #6b7280; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 5px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + PREFIX + ' { padding: 16px 18px 24px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function makeFormatter(currency) {
    if (/^[A-Za-z]{3}$/.test(currency)) {
      try {
        var fmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: currency.toUpperCase() });
        return function (n) { return fmt.format(n); };
      } catch (e) { /* fall through to symbol prefix */ }
    }
    return function (n) { return currency + (Math.round(n * 100) / 100); };
  }

  function parseAmount(script, attr, fallback) {
    var raw = script.getAttribute(attr);
    if (raw == null || raw === '') return fallback;
    var n = parseFloat(raw);
    if (isNaN(n) || n < 0) {
      console.warn('[haynes lab free-shipping-progress] ' + attr + '="' + raw + '" is not a valid non-negative number. Using ' + fallback + '.');
      return fallback;
    }
    return n;
  }

  function build(script) {
    var goal = parseAmount(script, 'data-hl-goal', 50);
    var current = parseAmount(script, 'data-hl-current', 0);
    var currency = script.getAttribute('data-hl-currency') || 'USD';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';
    var format = makeFormatter(currency);

    if (goal <= 0) {
      console.warn('[haynes lab free-shipping-progress] data-hl-goal must be greater than 0. Nothing rendered.');
      return;
    }

    var reached = current >= goal;
    var pct = Math.min(100, Math.round((current / goal) * 100));

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);

    var message = el('p', PREFIX + '__message');
    if (reached) {
      var check = el('span', PREFIX + '__check', '✓');
      check.setAttribute('aria-hidden', 'true');
      message.appendChild(check);
      var strong = el('strong', null, 'You’ve unlocked FREE shipping!');
      message.appendChild(strong);
    } else {
      message.appendChild(document.createTextNode('You’re '));
      message.appendChild(el('strong', null, format(goal - current)));
      message.appendChild(document.createTextNode(' away from free shipping'));
    }
    root.appendChild(message);

    var track = el('div', PREFIX + '__track');
    track.setAttribute('role', 'progressbar');
    track.setAttribute('aria-valuemin', '0');
    track.setAttribute('aria-valuemax', String(goal));
    track.setAttribute('aria-valuenow', String(Math.min(current, goal)));
    track.setAttribute('aria-label', reached
      ? 'Free shipping unlocked'
      : 'Progress toward free shipping: ' + format(current) + ' of ' + format(goal));
    var fill = el('div', PREFIX + '__fill');
    track.appendChild(fill);
    root.appendChild(track);

    var meta = el('div', PREFIX + '__meta');
    meta.appendChild(el('span', null, format(current) + ' in cart'));
    meta.appendChild(el('span', null, pct + '% of ' + format(goal)));
    root.appendChild(meta);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/free-shipping-progress';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }

    // Animate the fill after insert so the transition is visible.
    setTimeout(function () { fill.style.width = pct + '%'; }, 50);
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab free-shipping-progress] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-goal"50"The order amount that unlocks free shipping, in your store currency.
data-hl-current"0"The shopper's current cart total; at or above the goal the success state shows.
data-hl-currency"USD"A 3-letter ISO currency code (e.g. USD, EUR, GBP) or a plain symbol like "$" or "€".
data-hl-accent"#f97316"Hex color for the progress bar fill and success check.

Frequently asked questions

How do I install the Free Shipping Progress widget?

Paste the script tag where you want the bar to appear — typically on your cart page or cart drawer. Set data-hl-goal to your free-shipping threshold and data-hl-current to the shopper's cart total, and the widget renders inline at that exact spot.

Can I customize the colors and currency?

Yes. Use data-hl-accent to set the bar and success-check color, and data-hl-currency for any 3-letter ISO code (USD, EUR, GBP…) or a plain symbol like "€". Amounts are formatted automatically.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it's a single vanilla JS file with no dependencies. Paste the script tag into any HTML block, theme template, or code-injection area and it works everywhere.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch