Business & Operations

Free Pricing Table Widget

Responsive pricing tiers with a monthly/annual billing toggle.

A clean, responsive pricing table you define with a small JSON config: two to four tiers with monthly and annual prices, feature lists, and call-to-action links. A built-in toggle lets visitors switch between monthly and annual billing, with an automatic savings percentage, and one tier can be highlighted as the popular pick. Install it by pasting a single script tag anywhere on your page.

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/pricing-table.js" data-hl-config='{"tiers":[{"name":"Starter","monthly":"19","annual":"15","features":["1 website","Basic analytics","Email support"],"cta":"Start free","href":"#starter"},{"name":"Pro","monthly":"49","annual":"39","features":["5 websites","Advanced analytics","Custom domains","Priority support"],"cta":"Get Pro","href":"#pro"},{"name":"Business","monthly":"99","annual":"79","features":["Unlimited websites","Team seats","API access","Dedicated manager"],"cta":"Contact sales","href":"#business"}],"highlight":1}' async></script>
<!-- Free Pricing Table widget by haynes lab — https://hayneslab.dev/tools/widgets/pricing-table -->
Inline (self-contained)html
<!-- Free Pricing Table widget by haynes lab — https://hayneslab.dev/tools/widgets/pricing-table -->
<script data-hl-config='{"tiers":[{"name":"Starter","monthly":"19","annual":"15","features":["1 website","Basic analytics","Email support"],"cta":"Start free","href":"#starter"},{"name":"Pro","monthly":"49","annual":"39","features":["5 websites","Advanced analytics","Custom domains","Priority support"],"cta":"Get Pro","href":"#pro"},{"name":"Business","monthly":"99","annual":"79","features":["Unlimited websites","Team seats","API access","Dedicated manager"],"cta":"Contact sales","href":"#business"}],"highlight":1}'>
/*!
 * haynes lab — Pricing Table widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/pricing-table
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-pricing-table';
  var STYLE_ID = 'hlw-pricing-table-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 1024px; margin: 0 auto; padding: 32px 24px 40px;' +
      ' 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 + '__toggle { display: flex; align-items: center; justify-content: center; gap: 12px;' +
      ' margin-bottom: 28px; font-size: 15px; font-weight: 600; color: #6b7280; }' +
      '.' + PREFIX + '__period { padding: 0; border: 0; background: none; font: inherit; color: inherit;' +
      ' cursor: pointer; }' +
      '.' + PREFIX + '__period[aria-pressed="true"] { color: #111827; }' +
      '.' + PREFIX + '__period:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px;' +
      ' border-radius: 4px; }' +
      '.' + PREFIX + '__switch { width: 46px; height: 25px; border-radius: 13px; border: 1px solid #d1d5db;' +
      ' background: #e5e7eb; cursor: pointer; padding: 2px; display: inline-flex; align-items: center; }' +
      '.' + PREFIX + '__switch-knob { width: 19px; height: 19px; border-radius: 50%; background: #ffffff;' +
      ' box-shadow: 0 1px 2px rgba(0,0,0,0.15); transition: transform 0.2s ease; }' +
      '.' + PREFIX + '__switch[aria-pressed="true"] { background: var(--hlw-accent);' +
      ' border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__switch[aria-pressed="true"] .' + PREFIX + '__switch-knob { transform: translateX(19px); }' +
      '.' + PREFIX + '__switch:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__grid { display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }' +
      '.' + PREFIX + '__tier { position: relative; display: flex; flex-direction: column; padding: 24px 22px;' +
      ' border: 1px solid #e5e7eb; border-radius: 10px; background: #ffffff; }' +
      '.' + PREFIX + '__tier--highlight { border-color: var(--hlw-accent);' +
      ' box-shadow: 0 4px 14px rgba(0,0,0,0.07); }' +
      '.' + PREFIX + '__badge { position: absolute; top: -11px; left: 50%; transform: translateX(-50%);' +
      ' padding: 2px 10px; border-radius: 999px; background: var(--hlw-accent); color: #ffffff;' +
      ' font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;' +
      ' white-space: nowrap; }' +
      '.' + PREFIX + '__name { margin: 0 0 10px; font-size: 17px; font-weight: 700; color: #111827;' +
      ' text-align: center; }' +
      '.' + PREFIX + '__price { text-align: center; margin-bottom: 18px; }' +
      '.' + PREFIX + '__amount { font-size: 34px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__per { font-size: 14px; color: #6b7280; }' +
      '.' + PREFIX + '__save { display: block; min-height: 18px; font-size: 12px; font-weight: 600;' +
      ' color: var(--hlw-accent); }' +
      '.' + PREFIX + '__features { margin: 0 0 22px; padding: 0; list-style: none; flex: 1; }' +
      '.' + PREFIX + '__feature { position: relative; padding: 5px 0 5px 24px; font-size: 14px;' +
      ' color: #374151; }' +
      '.' + PREFIX + '__feature::before { content: "\\2713"; position: absolute; left: 0; top: 5px;' +
      ' font-weight: 700; color: var(--hlw-accent); }' +
      '.' + PREFIX + '__cta { display: block; padding: 11px 16px; border-radius: 8px; text-align: center;' +
      ' font-size: 15px; font-weight: 600; text-decoration: none; color: #ffffff;' +
      ' background: #111827; transition: filter 0.15s ease; }' +
      '.' + PREFIX + '__cta:hover { filter: brightness(1.15); }' +
      '.' + PREFIX + '__cta:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__tier--highlight .' + PREFIX + '__cta { background: var(--hlw-accent); }' +
      '.' + 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: 24px 14px 38px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function parseConfig(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[haynes lab pricing-table] Missing required data-hl-config attribute (JSON {"tiers":[...],"highlight":index}). Nothing rendered.');
      return null;
    }
    var cfg;
    try {
      cfg = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab pricing-table] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    if (!cfg || !Array.isArray(cfg.tiers)) {
      console.warn('[haynes lab pricing-table] data-hl-config must contain a "tiers" array. Nothing rendered.');
      return null;
    }
    var tiers = cfg.tiers.filter(function (t) {
      return t && typeof t.name === 'string' && t.name &&
        t.monthly != null && String(t.monthly) !== '';
    });
    if (tiers.length < 2 || tiers.length > 4) {
      console.warn('[haynes lab pricing-table] data-hl-config must define between 2 and 4 valid tiers. Nothing rendered.');
      return null;
    }
    var highlight = parseInt(cfg.highlight, 10);
    if (isNaN(highlight) || highlight < 0 || highlight >= tiers.length) highlight = -1;
    return { tiers: tiers, highlight: highlight };
  }

  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 formatPrice(value, currency) {
    var str = String(value);
    if (isNaN(parseFloat(str))) return str;
    return currency + str;
  }

  function build(script) {
    var cfg = parseConfig(script);
    if (!cfg) return;

    var accent = script.getAttribute('data-hl-accent') || '#f97316';
    var currency = script.getAttribute('data-hl-currency') || '$';
    var tiers = cfg.tiers;

    injectStyles();

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

    var annual = false;
    var priceNodes = [];
    var saveNodes = [];

    function renderPrices() {
      tiers.forEach(function (t, i) {
        var value = annual && t.annual != null && String(t.annual) !== '' ? t.annual : t.monthly;
        priceNodes[i].textContent = formatPrice(value, currency);
        var save = '';
        if (annual && !isNaN(parseFloat(t.monthly)) && !isNaN(parseFloat(t.annual))) {
          var pct = Math.round((1 - parseFloat(t.annual) / parseFloat(t.monthly)) * 100);
          if (pct > 0) save = 'Save ' + pct + '%';
        }
        saveNodes[i].textContent = save;
      });
    }

    function setPeriod(nextAnnual) {
      annual = nextAnnual;
      switchBtn.setAttribute('aria-pressed', annual ? 'true' : 'false');
      monthlyBtn.setAttribute('aria-pressed', annual ? 'false' : 'true');
      annualBtn.setAttribute('aria-pressed', annual ? 'true' : 'false');
      renderPrices();
    }

    var toggle = el('div', PREFIX + '__toggle');
    var monthlyBtn = el('button', PREFIX + '__period', 'Monthly');
    monthlyBtn.type = 'button';
    monthlyBtn.setAttribute('aria-pressed', 'true');
    var switchBtn = el('button', PREFIX + '__switch');
    switchBtn.type = 'button';
    switchBtn.setAttribute('aria-pressed', 'false');
    switchBtn.setAttribute('aria-label', 'Toggle annual pricing');
    switchBtn.appendChild(el('span', PREFIX + '__switch-knob'));
    var annualBtn = el('button', PREFIX + '__period', 'Annual');
    annualBtn.type = 'button';
    annualBtn.setAttribute('aria-pressed', 'false');
    monthlyBtn.addEventListener('click', function () { setPeriod(false); });
    annualBtn.addEventListener('click', function () { setPeriod(true); });
    switchBtn.addEventListener('click', function () { setPeriod(!annual); });
    toggle.appendChild(monthlyBtn);
    toggle.appendChild(switchBtn);
    toggle.appendChild(annualBtn);
    root.appendChild(toggle);

    var grid = el('div', PREFIX + '__grid');
    tiers.forEach(function (t, i) {
      var tier = el('div', PREFIX + '__tier' + (i === cfg.highlight ? ' ' + PREFIX + '__tier--highlight' : ''));
      if (i === cfg.highlight) tier.appendChild(el('span', PREFIX + '__badge', 'Most popular'));
      tier.appendChild(el('h3', PREFIX + '__name', t.name));

      var price = el('div', PREFIX + '__price');
      var amount = el('span', PREFIX + '__amount');
      price.appendChild(amount);
      price.appendChild(el('span', PREFIX + '__per', '/mo'));
      var save = el('span', PREFIX + '__save');
      price.appendChild(save);
      tier.appendChild(price);
      priceNodes.push(amount);
      saveNodes.push(save);

      var list = el('ul', PREFIX + '__features');
      var feats = Array.isArray(t.features) ? t.features : [];
      feats.forEach(function (f) {
        if (f != null && String(f) !== '') list.appendChild(el('li', PREFIX + '__feature', String(f)));
      });
      tier.appendChild(list);

      var cta = el('a', PREFIX + '__cta', typeof t.cta === 'string' && t.cta ? t.cta : 'Get started');
      cta.href = typeof t.href === 'string' && t.href ? t.href : '#';
      tier.appendChild(cta);

      grid.appendChild(tier);
    });
    root.appendChild(grid);
    renderPrices();

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/pricing-table';
    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 pricing-table] 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 object with a "tiers" array (2-4 items, each with name, monthly, annual, features array, cta, href) and an optional "highlight" index for the tier marked as most popular.
data-hl-accent"#f97316"Color for the highlighted tier border, "Most popular" badge, feature checkmarks, and toggle switch.
data-hl-currency"$"Symbol prepended to numeric prices, e.g. "$", "€", or "£".

Frequently asked questions

How do I install the pricing table?

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

Can I customize the tiers, prices, and colors?

Yes — define 2-4 tiers in data-hl-config with monthly and annual prices, feature lists, and CTA links, set "highlight" to the index of your featured tier, and use data-hl-accent and data-hl-currency to match your brand and currency.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on any platform that lets you paste a script tag, including WordPress, Shopify, and Squarespace.

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