E-commerce & Retail

Free Stock Level Indicator Widget

An "Only X left in stock" urgency bar that turns red as inventory drops.

A compact low-stock badge with a progress bar that fills in proportion to your remaining inventory. The bar shifts from your brand color to amber to red as stock runs low, nudging shoppers to buy before it sells out. Paste one script tag next to your product price or add-to-cart button — no plugins, no accounts.

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/stock-level-indicator.js" data-hl-stock="4" data-hl-max="20" async></script>
<!-- Free Stock Level Indicator widget by haynes lab — https://hayneslab.dev/tools/widgets/stock-level-indicator -->
Inline (self-contained)html
<!-- Free Stock Level Indicator widget by haynes lab — https://hayneslab.dev/tools/widgets/stock-level-indicator -->
<script data-hl-stock="4" data-hl-max="20">
/*!
 * haynes lab — Stock Level Indicator widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/stock-level-indicator
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-stock-level-indicator';
  var STYLE_ID = 'hlw-stock-level-indicator-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/stock-level-indicator.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: 18px 20px 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 + '__text { margin: 0 0 10px; font-size: 15px; color: #374151; }' +
      '.' + PREFIX + '__count { font-weight: 700; color: var(--hlw-level); }' +
      '.' + PREFIX + '__track { height: 8px; border-radius: 999px; background: #f3f4f6; overflow: hidden; }' +
      '.' + PREFIX + '__fill { height: 100%; border-radius: 999px; background: var(--hlw-level);' +
      ' transition: width 0.6s ease, background 0.3s ease; }' +
      '.' + 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: 560px) { .' + PREFIX + ' { padding: 16px 16px 26px; } }';
    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 build(script) {
    var stock = parseInt(script.getAttribute('data-hl-stock'), 10);
    if (isNaN(stock) || stock < 0) {
      console.warn('[haynes lab stock-level-indicator] Missing or invalid required data-hl-stock attribute (non-negative integer). Nothing rendered.');
      return;
    }
    var max = parseInt(script.getAttribute('data-hl-max'), 10);
    if (isNaN(max) || max < 1) max = 100;
    if (stock > max) max = stock;

    var template = script.getAttribute('data-hl-text') || 'Only {stock} left in stock — order soon';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    var ratio = stock / max;
    var level = accent;
    if (stock === 0 || ratio <= 0.2) level = '#ef4444';
    else if (ratio <= 0.5) level = '#f59e0b';

    injectStyles();

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

    var text = el('p', PREFIX + '__text');
    if (stock === 0) {
      text.textContent = 'Out of stock';
    } else {
      var parts = template.split('{stock}');
      for (var i = 0; i < parts.length; i++) {
        var segment = parts[i].replace(/\{max\}/g, String(max));
        if (segment) text.appendChild(document.createTextNode(segment));
        if (i < parts.length - 1) {
          text.appendChild(el('span', PREFIX + '__count', String(stock)));
        }
      }
    }
    root.appendChild(text);

    var track = el('div', PREFIX + '__track');
    track.setAttribute('role', 'progressbar');
    track.setAttribute('aria-label', 'Stock level');
    track.setAttribute('aria-valuemin', '0');
    track.setAttribute('aria-valuemax', String(max));
    track.setAttribute('aria-valuenow', String(stock));
    var fill = el('div', PREFIX + '__fill');
    fill.style.width = Math.round(ratio * 100) + '%';
    track.appendChild(fill);
    root.appendChild(track);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/stock-level-indicator';
    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 stock-level-indicator] 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-stock(required)Current number of units left in stock. Must be a non-negative integer.
data-hl-max"100"The full-stock quantity the bar fill is measured against. Defaults to 100.
data-hl-text"Only {stock} left in stock — order soon"Message above the bar. Use {stock} and {max} as placeholders for the live numbers.
data-hl-accent"#f97316"Bar and count color while stock is healthy (above 50%). Drops to amber, then red, automatically.

Frequently asked questions

How do I install the stock level indicator?

Copy the embed snippet and paste it where you want the indicator to appear on your product page — typically near the price or add-to-cart button. Set data-hl-stock to the units remaining and it renders inline at that spot.

How do I customize the message and colors?

Everything is controlled through data-hl-* attributes on the script tag. Use data-hl-text with {stock} and {max} placeholders for the message, data-hl-max for the full-stock quantity, and data-hl-accent for the healthy-stock color. The bar automatically shifts to amber below 50% and red below 20% (or when sold out).

Does it work on WordPress, Shopify, or Squarespace?

Yes. The widget is a single dependency-free vanilla JS file, so it runs anywhere you can paste a script tag — a Custom HTML block in WordPress or Squarespace, a Shopify product template or theme section, Webflow, or 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