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 Loan Payment Calculator widget by haynes lab — https://hayneslab.dev/tools/widgets/loan-payment-calculator -->
<script data-hl-extra="100" data-hl-title="Auto Loan Calculator">
/*!
* haynes lab — Loan Payment Calculator widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/loan-payment-calculator
*/
(function () {
'use strict';
var PREFIX = 'hlw-loan-payment-calculator';
var STYLE_ID = 'hlw-loan-payment-calculator-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/loan-payment-calculator.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { position: relative; max-width: 560px; margin: 0 auto; padding: 28px 28px 36px;' +
' 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 20px; font-size: 19px; font-weight: 700; color: #111827; text-align: center; }' +
'.' + PREFIX + '__fields { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px; }' +
'.' + PREFIX + '__field { display: flex; flex-direction: column; gap: 5px; }' +
'.' + PREFIX + '__label { font-size: 13px; font-weight: 600; color: #374151; }' +
'.' + PREFIX + '__input { width: 100%; padding: 9px 12px; font: inherit; color: #111827;' +
' background: #ffffff; border: 1px solid #d1d5db; border-radius: 8px; }' +
'.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
'.' + PREFIX + '__results { padding: 18px 20px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; }' +
'.' + PREFIX + '__payment { text-align: center; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #e5e7eb; }' +
'.' + PREFIX + '__payment-value { display: block; font-size: 34px; font-weight: 700; color: var(--hlw-accent);' +
' font-variant-numeric: tabular-nums; line-height: 1.1; }' +
'.' + PREFIX + '__payment-label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #6b7280; }' +
'.' + PREFIX + '__row { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; font-size: 14px; }' +
'.' + PREFIX + '__row-label { color: #6b7280; }' +
'.' + PREFIX + '__row-value { font-weight: 600; color: #111827; font-variant-numeric: tabular-nums; text-align: right; }' +
'.' + PREFIX + '__row--savings .' + PREFIX + '__row-value { 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 18px 34px; }' +
' .' + PREFIX + '__fields { 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 num(value, fallback) {
var n = parseFloat(value);
return isNaN(n) ? fallback : n;
}
function build(script) {
var title = script.getAttribute('data-hl-title');
if (title === null) title = 'Loan Payment Calculator';
var currency = script.getAttribute('data-hl-currency') || '$';
var accent = script.getAttribute('data-hl-accent') || '#f97316';
var defaults = {
amount: num(script.getAttribute('data-hl-amount'), 25000),
rate: num(script.getAttribute('data-hl-rate'), 7),
term: num(script.getAttribute('data-hl-term'), 60),
extra: num(script.getAttribute('data-hl-extra'), 0)
};
injectStyles();
var root = el('div', PREFIX);
root.style.setProperty('--hlw-accent', accent);
if (title) root.appendChild(el('h3', PREFIX + '__title', title));
var fields = el('div', PREFIX + '__fields');
var inputs = {};
var idBase = PREFIX + '-' + Math.random().toString(36).slice(2, 8) + '-';
[
{ key: 'amount', label: 'Loan amount', step: 'any', min: '0' },
{ key: 'rate', label: 'Interest rate (% APR)', step: 'any', min: '0' },
{ key: 'term', label: 'Term (months)', step: '1', min: '1' },
{ key: 'extra', label: 'Extra monthly payment', step: 'any', min: '0' }
].forEach(function (f) {
var field = el('div', PREFIX + '__field');
var label = el('label', PREFIX + '__label', f.label);
var input = el('input', PREFIX + '__input');
input.type = 'number';
input.min = f.min;
input.step = f.step;
input.value = String(defaults[f.key]);
input.id = idBase + f.key;
label.setAttribute('for', input.id);
field.appendChild(label);
field.appendChild(input);
fields.appendChild(field);
inputs[f.key] = input;
input.addEventListener('input', update);
});
root.appendChild(fields);
var results = el('div', PREFIX + '__results');
results.setAttribute('aria-live', 'polite');
var paymentBox = el('div', PREFIX + '__payment');
var paymentValue = el('span', PREFIX + '__payment-value');
paymentBox.appendChild(paymentValue);
paymentBox.appendChild(el('span', PREFIX + '__payment-label', 'Monthly payment'));
results.appendChild(paymentBox);
var rows = {};
[
['interest', 'Total interest'],
['payoff', 'Payoff time'],
['extraPayoff', 'Payoff with extra payments'],
['saved', 'Interest saved by extra payments']
].forEach(function (r) {
var row = el('div', PREFIX + '__row' + (r[0] === 'saved' ? ' ' + PREFIX + '__row--savings' : ''));
row.appendChild(el('span', PREFIX + '__row-label', r[1]));
var value = el('span', PREFIX + '__row-value');
row.appendChild(value);
results.appendChild(row);
rows[r[0]] = { row: row, value: value };
});
root.appendChild(results);
function money(n) {
return currency + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatMonths(months) {
var years = Math.floor(months / 12);
var rem = months % 12;
if (!years) return rem + ' mo';
if (!rem) return years + (years === 1 ? ' yr' : ' yrs');
return years + (years === 1 ? ' yr ' : ' yrs ') + rem + ' mo';
}
// Months to pay off `balance` at monthly rate `r` paying `payment` per month.
function monthsToPayoff(balance, r, payment) {
if (payment <= balance * r) return Infinity; // never pays off
if (r === 0) return Math.ceil(balance / payment);
return Math.ceil(Math.log(payment / (payment - r * balance)) / Math.log(1 + r));
}
function update() {
var amount = Math.max(0, num(inputs.amount.value, 0));
var rate = Math.max(0, num(inputs.rate.value, 0));
var term = Math.max(1, Math.round(num(inputs.term.value, 0)));
var extra = Math.max(0, num(inputs.extra.value, 0));
var r = rate / 100 / 12;
if (amount <= 0) {
paymentValue.textContent = '—';
['interest', 'payoff', 'extraPayoff', 'saved'].forEach(function (k) {
rows[k].row.style.display = 'none';
});
return;
}
var payment = r === 0 ? amount / term : amount * r / (1 - Math.pow(1 + r, -term));
var totalInterest = payment * term - amount;
paymentValue.textContent = money(payment);
rows.interest.value.textContent = money(totalInterest);
rows.interest.row.style.display = '';
rows.payoff.value.textContent = formatMonths(term);
rows.payoff.row.style.display = '';
if (extra > 0) {
var months = monthsToPayoff(amount, r, payment + extra);
if (isFinite(months)) {
var interestWithExtra = (payment + extra) * months - amount;
// Final month is a partial payment; approximate by capping at remaining balance.
if (interestWithExtra < 0) interestWithExtra = 0;
rows.extraPayoff.value.textContent = formatMonths(months) +
(months < term ? ' (' + formatMonths(term - months) + ' sooner)' : '');
rows.extraPayoff.row.style.display = '';
rows.saved.value.textContent = money(Math.max(0, totalInterest - interestWithExtra));
rows.saved.row.style.display = '';
} else {
rows.extraPayoff.row.style.display = 'none';
rows.saved.row.style.display = 'none';
}
} else {
rows.extraPayoff.row.style.display = 'none';
rows.saved.row.style.display = 'none';
}
}
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/loan-payment-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 loan-payment-calculator] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>