E-commerce & Retail

Free Shipping Estimator Widget

Let shoppers check shipping options and costs by ZIP/postcode or country.

A lightweight shipping estimator that shows visitors which delivery options reach their ZIP/postcode or country, with prices and ETAs pulled from your own JSON config. Built for e-commerce stores that want to answer "do you ship to me?" before checkout. Install by pasting a single script tag where you want the estimator to appear.

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/shipping-estimator.js" data-hl-config='[{"name":"Standard","price":4.95,"eta":"4–7 business days","regions":["worldwide"]},{"name":"Express","price":12.95,"eta":"2–3 business days","regions":["US","CA"]},{"name":"Overnight","price":24.95,"eta":"Next business day","regions":["US"]}]' data-hl-title='"Shipping Estimator"' data-hl-currency='"USD"' data-hl-free-over="75" async></script>
<!-- Free Shipping Estimator widget by haynes lab — https://hayneslab.dev/tools/widgets/shipping-estimator -->
Inline (self-contained)html
<!-- Free Shipping Estimator widget by haynes lab — https://hayneslab.dev/tools/widgets/shipping-estimator -->
<script data-hl-config='[{"name":"Standard","price":4.95,"eta":"4–7 business days","regions":["worldwide"]},{"name":"Express","price":12.95,"eta":"2–3 business days","regions":["US","CA"]},{"name":"Overnight","price":24.95,"eta":"Next business day","regions":["US"]}]' data-hl-title='"Shipping Estimator"' data-hl-currency='"USD"' data-hl-free-over="75">
/*!
 * haynes lab — Shipping Estimator widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/shipping-estimator
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-shipping-estimator';
  var STYLE_ID = 'hlw-shipping-estimator-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/shipping-estimator.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: 24px 24px 32px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__title { margin: 0 0 16px; font-size: 18px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__form { display: flex; gap: 8px; }' +
      '.' + PREFIX + '__input { flex: 1; min-width: 0; padding: 10px 12px; font-size: 15px; font-family: inherit;' +
      ' color: #1f2937; background: #ffffff; border: 1px solid #d1d5db; border-radius: 8px; }' +
      '.' + PREFIX + '__input:focus { outline: 2px solid #f97316; outline-offset: 1px; border-color: #f97316; }' +
      '.' + PREFIX + '__button { padding: 10px 18px; font-size: 15px; font-weight: 600; font-family: inherit;' +
      ' color: #ffffff; background: #f97316; border: 0; border-radius: 8px; cursor: pointer; }' +
      '.' + PREFIX + '__button:hover { background: #ea580c; }' +
      '.' + PREFIX + '__button:focus-visible { outline: 2px solid #f97316; outline-offset: 2px; }' +
      '.' + PREFIX + '__results { margin-top: 16px; }' +
      '.' + PREFIX + '__option { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px;' +
      ' border: 1px solid #e5e7eb; border-radius: 8px; }' +
      '.' + PREFIX + '__option + .' + PREFIX + '__option { margin-top: 8px; }' +
      '.' + PREFIX + '__name { font-weight: 600; font-size: 15px; color: #111827; }' +
      '.' + PREFIX + '__eta { flex: 1; font-size: 13px; color: #6b7280; }' +
      '.' + PREFIX + '__price { font-weight: 700; font-size: 15px; color: #111827; white-space: nowrap; }' +
      '.' + PREFIX + '__empty { margin: 0; padding: 12px 14px; font-size: 14px; color: #6b7280;' +
      ' background: #f9fafb; border: 1px dashed #d1d5db; border-radius: 8px; }' +
      '.' + PREFIX + '__note { margin: 12px 0 0; font-size: 13px; color: #059669; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 6px; 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: 480px) { .' + PREFIX + ' { padding: 20px 16px 32px; }' +
      ' .' + PREFIX + '__form { flex-direction: column; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function parseOptions(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[haynes lab shipping-estimator] Missing required data-hl-config attribute (JSON array of {name, price, eta, regions}). Nothing rendered.');
      return null;
    }
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab shipping-estimator] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab shipping-estimator] data-hl-config must be a JSON array. Nothing rendered.');
      return null;
    }
    var valid = items.filter(function (o) {
      return o && typeof o.name === 'string' && o.name && typeof o.price === 'number' && o.price >= 0;
    }).map(function (o) {
      return {
        name: o.name,
        price: o.price,
        eta: typeof o.eta === 'string' ? o.eta : '',
        regions: Array.isArray(o.regions) ? o.regions : []
      };
    });
    if (!valid.length) {
      console.warn('[haynes lab shipping-estimator] data-hl-config has no valid options (each needs at least "name" and numeric "price"). Nothing rendered.');
      return null;
    }
    return valid;
  }

  function regionMatches(regions, input) {
    if (!regions.length) return true;
    for (var i = 0; i < regions.length; i++) {
      var region = String(regions[i]).trim().toLowerCase();
      if (!region) continue;
      if (region === '*' || region === 'worldwide' || region === 'global') return true;
      if (input === region) return true;
      if (input.length > region.length && input.indexOf(region) === 0) return true;
    }
    return false;
  }

  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 build(script) {
    var options = parseOptions(script);
    if (!options) return;

    var title = script.getAttribute('data-hl-title') || 'Shipping Estimator';
    var currency = (script.getAttribute('data-hl-currency') || 'USD').toUpperCase();
    var freeOver = parseFloat(script.getAttribute('data-hl-free-over'));

    function formatPrice(price) {
      if (price === 0) return 'Free';
      try {
        return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency }).format(price);
      } catch (e) {
        return currency + ' ' + price.toFixed(2);
      }
    }

    injectStyles();

    var root = el('div', PREFIX);
    root.appendChild(el('h3', PREFIX + '__title', title));

    var input = el('input', PREFIX + '__input');
    input.type = 'text';
    input.placeholder = 'Enter ZIP/postcode or country';
    input.setAttribute('aria-label', 'ZIP/postcode or country');

    var button = el('button', PREFIX + '__button', 'Check');
    button.type = 'submit';

    var form = el('form', PREFIX + '__form');
    form.appendChild(input);
    form.appendChild(button);
    root.appendChild(form);

    var results = el('div', PREFIX + '__results');
    results.setAttribute('aria-live', 'polite');
    root.appendChild(results);

    function render() {
      var query = input.value.trim().toLowerCase();
      results.textContent = '';
      if (!query) {
        input.focus();
        return;
      }
      var matches = options.filter(function (o) { return regionMatches(o.regions, query); });
      if (!matches.length) {
        results.appendChild(el('p', PREFIX + '__empty',
          'Sorry, we don\'t ship to "' + input.value.trim() + '" yet.'));
        return;
      }
      matches.forEach(function (o) {
        var row = el('div', PREFIX + '__option');
        row.appendChild(el('span', PREFIX + '__name', o.name));
        if (o.eta) row.appendChild(el('span', PREFIX + '__eta', o.eta));
        row.appendChild(el('span', PREFIX + '__price', formatPrice(o.price)));
        results.appendChild(row);
      });
    }

    form.addEventListener('submit', function (e) {
      e.preventDefault();
      render();
    });

    if (!isNaN(freeOver) && freeOver > 0) {
      root.appendChild(el('p', PREFIX + '__note',
        'Free shipping on orders over ' + formatPrice(freeOver) + '.'));
    }

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/shipping-estimator';
    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);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab shipping-estimator] 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-config(required)JSON array of shipping options, each with name, numeric price, eta, and a regions array of country names/codes or ZIP prefixes (use "worldwide" or an empty array to match everywhere).
data-hl-title"Shipping Estimator"Heading shown above the input field.
data-hl-currency"USD"ISO currency code used to format option prices (e.g. "EUR", "GBP").
data-hl-free-over(none)Order subtotal above which a "free shipping" note is shown (e.g. "75").

Frequently asked questions

How do I install the shipping estimator?

Paste the script tag with your data-hl-config JSON where you want the estimator to appear — it renders inline right after the tag.

Can I change the currency, title, or free-shipping note?

Yes — use data-hl-currency for price formatting, data-hl-title for the heading, and data-hl-free-over to show a free-shipping threshold note.

How does region matching work?

Each option lists regions such as country names, country codes, or ZIP prefixes. An option matches when the visitor's input equals or starts with a region; "worldwide" or an empty regions array matches everywhere.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it's a single vanilla JS file with no dependencies, so it works anywhere you can paste a script tag, including WordPress, Shopify, Squarespace, and plain HTML.

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