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.
<!-- Free Compound Interest Calculator widget by haynes lab — https://hayneslab.dev/tools/widgets/compound-interest-calculator -->
<script>
/*!
* haynes lab — Compound Interest Calculator widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/compound-interest-calculator
*/
(function () {
'use strict';
var PREFIX = 'hlw-compound-interest-calculator';
var STYLE_ID = 'hlw-compound-interest-calculator-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/compound-interest-calculator.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { position: relative; max-width: 640px; margin: 0 auto; padding: 24px 24px 32px;' +
' 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 + '__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }' +
'.' + PREFIX + '__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }' +
'.' + PREFIX + '__field--full { grid-column: 1 / -1; }' +
'.' + PREFIX + '__label { font-size: 12px; font-weight: 600; color: #6b7280;' +
' text-transform: uppercase; letter-spacing: 0.04em; }' +
'.' + PREFIX + '__input { width: 100%; padding: 8px 10px; font: inherit; color: #111827;' +
' background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; }' +
'.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px;' +
' border-color: var(--hlw-accent); background: #ffffff; }' +
'.' + PREFIX + '__results { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;' +
' margin-top: 18px; }' +
'.' + PREFIX + '__stat { padding: 12px; background: #f9fafb; border: 1px solid #e5e7eb;' +
' border-radius: 8px; text-align: center; }' +
'.' + PREFIX + '__stat-value { display: block; font-size: 18px; font-weight: 700; color: #111827;' +
' word-break: break-word; }' +
'.' + PREFIX + '__stat--balance .' + PREFIX + '__stat-value { color: var(--hlw-accent); font-size: 22px; }' +
'.' + PREFIX + '__stat-label { display: block; font-size: 11px; color: #6b7280; margin-top: 2px;' +
' text-transform: uppercase; letter-spacing: 0.04em; }' +
'.' + PREFIX + '__chart { display: block; width: 100%; height: 180px; margin-top: 18px; }' +
'.' + 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: 18px 16px 30px; }' +
' .' + PREFIX + '__grid, .' + PREFIX + '__results { grid-template-columns: 1fr; } }';
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 numAttr(script, name, fallback) {
var n = parseFloat(script.getAttribute(name));
return isNaN(n) ? fallback : n;
}
function build(script) {
var defaults = {
principal: numAttr(script, 'data-hl-principal', 10000),
rate: numAttr(script, 'data-hl-rate', 7),
years: numAttr(script, 'data-hl-years', 20),
compounds: numAttr(script, 'data-hl-compounds', 12),
contribution: numAttr(script, 'data-hl-contribution', 200)
};
var currency = script.getAttribute('data-hl-currency') || 'USD';
var accent = script.getAttribute('data-hl-accent') || '#f97316';
var fmt;
try {
fmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: currency, maximumFractionDigits: 0 });
} catch (e) {
console.warn('[haynes lab compound-interest-calculator] Unknown currency "' + currency + '", falling back to USD.');
fmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
}
injectStyles();
var root = el('div', PREFIX);
root.style.setProperty('--hlw-accent', accent);
var grid = el('div', PREFIX + '__grid');
var inputs = {};
var fields = [
{ key: 'principal', label: 'Initial principal', min: '0', step: 'any' },
{ key: 'rate', label: 'Annual rate (%)', min: '0', step: 'any' },
{ key: 'years', label: 'Years', min: '1', step: '1' },
{ key: 'compounds', label: 'Compounds per year', min: '1', step: '1' },
{ key: 'contribution', label: 'Monthly contribution', min: '0', step: 'any' }
];
fields.forEach(function (f) {
var wrap = el('div', PREFIX + '__field' + (f.key === 'contribution' ? ' ' + PREFIX + '__field--full' : ''));
var id = PREFIX + '-' + f.key + '-' + Math.random().toString(36).slice(2, 8);
var label = el('label', PREFIX + '__label', f.label);
label.setAttribute('for', id);
var input = el('input', PREFIX + '__input');
input.type = 'number';
input.id = id;
input.min = f.min;
input.step = f.step;
input.value = String(defaults[f.key]);
wrap.appendChild(label);
wrap.appendChild(input);
grid.appendChild(wrap);
inputs[f.key] = input;
});
root.appendChild(grid);
var results = el('div', PREFIX + '__results');
results.setAttribute('aria-live', 'polite');
var stats = {};
[['balance', 'Final balance', true], ['contributed', 'Total contributed', false], ['interest', 'Interest earned', false]]
.forEach(function (s) {
var box = el('div', PREFIX + '__stat' + (s[2] ? ' ' + PREFIX + '__stat--balance' : ''));
var value = el('span', PREFIX + '__stat-value', '—');
box.appendChild(value);
box.appendChild(el('span', PREFIX + '__stat-label', s[1]));
results.appendChild(box);
stats[s[0]] = value;
});
root.appendChild(results);
var canvas = el('canvas', PREFIX + '__chart');
canvas.setAttribute('role', 'img');
canvas.setAttribute('aria-label', 'Growth curve of balance over time');
root.appendChild(canvas);
function readInputs() {
var v = {};
for (var key in inputs) {
var n = parseFloat(inputs[key].value);
v[key] = isNaN(n) || n < 0 ? 0 : n;
}
if (v.years < 1) v.years = 1;
if (v.compounds < 1) v.compounds = 1;
v.years = Math.min(Math.floor(v.years), 100);
v.compounds = Math.min(Math.floor(v.compounds), 365);
return v;
}
// Yearly balances. Monthly contributions earn the effective monthly rate
// implied by the nominal annual rate compounded v.compounds times a year.
function series(v) {
var monthlyRate = Math.pow(1 + (v.rate / 100) / v.compounds, v.compounds / 12) - 1;
var points = [v.principal];
var balance = v.principal;
for (var y = 1; y <= v.years; y++) {
for (var m = 0; m < 12; m++) {
balance = balance * (1 + monthlyRate) + v.contribution;
}
points.push(balance);
}
return points;
}
function draw(points, v) {
var dpr = window.devicePixelRatio || 1;
var width = canvas.clientWidth || 560;
var height = 180;
canvas.width = width * dpr;
canvas.height = height * dpr;
var ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, width, height);
var padL = 8, padR = 8, padT = 10, padB = 18;
var plotW = width - padL - padR;
var plotH = height - padT - padB;
var max = Math.max.apply(null, points.concat([1]));
var invested = v.principal + v.contribution * 12 * v.years;
function x(i) { return padL + (i / (points.length - 1 || 1)) * plotW; }
function y(val) { return padT + plotH - (val / max) * plotH; }
// gridlines
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = 1;
for (var g = 1; g <= 3; g++) {
var gy = padT + (g / 4) * plotH;
ctx.beginPath();
ctx.moveTo(padL, gy);
ctx.lineTo(width - padR, gy);
ctx.stroke();
}
// invested principal line (dashed)
ctx.strokeStyle = '#9ca3af';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(padL, y(invested > 0 ? Math.min(invested, max) : 0));
ctx.lineTo(width - padR, y(invested > 0 ? Math.min(invested, max) : 0));
ctx.stroke();
ctx.setLineDash([]);
// area fill + growth curve
ctx.beginPath();
ctx.moveTo(x(0), y(points[0]));
for (var i = 1; i < points.length; i++) ctx.lineTo(x(i), y(points[i]));
ctx.strokeStyle = accent;
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineTo(x(points.length - 1), y(0));
ctx.lineTo(x(0), y(0));
ctx.closePath();
ctx.globalAlpha = 0.12;
ctx.fillStyle = accent;
ctx.fill();
ctx.globalAlpha = 1;
// axis labels
ctx.fillStyle = '#6b7280';
ctx.font = '11px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Year 0', padL, height - 5);
ctx.textAlign = 'right';
ctx.fillText('Year ' + v.years, width - padR, height - 5);
}
function update() {
var v = readInputs();
var points = series(v);
var finalBalance = points[points.length - 1];
var contributed = v.principal + v.contribution * 12 * v.years;
stats.balance.textContent = fmt.format(finalBalance);
stats.contributed.textContent = fmt.format(contributed);
stats.interest.textContent = fmt.format(Math.max(finalBalance - contributed, 0));
draw(points, v);
}
for (var key in inputs) {
inputs[key].addEventListener('input', update);
}
window.addEventListener('resize', update);
update();
var credit = el('a', PREFIX + '__credit');
credit.appendChild(logoSvg());
credit.appendChild(document.createTextNode('Powered by haynes lab'));
credit.href = 'https://hayneslab.dev/tools/widgets/compound-interest-calculator';
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 compound-interest-calculator] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>