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.
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.
<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 --><!-- 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.
| Attribute | Default | Description |
|---|---|---|
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
Feature Comparison Table
Us-vs-them checkmark table that shows exactly why you win.
View toolBusiness & OperationsKPI Counter
Animated count-up stats row that springs to life when scrolled into view.
View toolBusiness & OperationsJob Openings List
Show your open roles anywhere on your site with a simple JSON config.
View toolNeed 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