NNicheStackTutorials
← All tutorials
Frontend Tools

mortcalc: Amortization Table That Beat the Spreadsheet for Client Demos

Real estate landing page needed monthly payment + amortization without embedding a third-party iframe. Pure math, printable table.

·11 min read·Frontend Tools
mortcalc: Amortization Table That Beat the Spreadsheet for Client Demos cover

Use case: Static site for a mortgage broker. Calculator drives email capture — but math must be correct first.

Full calculator

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Mortgage Calculator</title>
  <style>
    body { font-family: system-ui; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
    .row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
    label { display: block; font-size: 14px; margin-bottom: .25rem; }
    input { width: 100%; padding: .4rem; }
    table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 1rem; }
    th, td { border: 1px solid #e2e8f0; padding: .35rem .5rem; text-align: right; }
    th:first-child, td:first-child { text-align: left; }
  </style>
</head>
<body>
  <h1>Mortgage Calculator</h1>
  <div class="row">
    <div><label>Loan amount ($)</label><input type="number" id="principal" value="350000"></div>
    <div><label>Annual rate (%)</label><input type="number" id="rate" value="6.5" step="0.01"></div>
    <div><label>Term (years)</label><input type="number" id="years" value="30"></div>
    <div><label>Extra monthly ($)</label><input type="number" id="extra" value="0"></div>
  </div>
  <button id="go" style="margin-top:1rem">Calculate</button>
  <p id="summary"></p>
  <table id="table" hidden><thead><tr><th>#</th><th>Payment</th><th>Principal</th><th>Interest</th><th>Balance</th></tr></thead><tbody></tbody></table>
  <script>
    document.getElementById('go').onclick = () => {
      const P = Number(document.getElementById('principal').value);
      const annual = Number(document.getElementById('rate').value) / 100;
      const years = Number(document.getElementById('years').value);
      const extra = Number(document.getElementById('extra').value) || 0;
      const n = years * 12;
      const r = annual / 12;
      if (!P || !years || r < 0) return;

      const payment = r === 0 ? P / n : (P * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
      let balance = P;
      const tbody = document.querySelector('#table tbody');
      tbody.innerHTML = '';
      let totalInterest = 0;

      for (let i = 1; i <= n && balance > 0.01; i++) {
        const interest = balance * r;
        let principalPaid = payment - interest + extra;
        if (principalPaid > balance) principalPaid = balance;
        balance -= principalPaid;
        totalInterest += interest;
        const tr = document.createElement('tr');
        tr.innerHTML = '<td>' + i + '</td><td>' + (principalPaid + interest).toFixed(2) + '</td><td>' + principalPaid.toFixed(2) + '</td><td>' + interest.toFixed(2) + '</td><td>' + Math.max(0, balance).toFixed(2) + '</td>';
        tbody.appendChild(tr);
      }

      document.getElementById('summary').textContent =
        'Monthly payment: $' + payment.toFixed(2) + (extra ? ' + $' + extra.toFixed(2) + ' extra' : '') +
        ' | Total interest: $' + totalInterest.toFixed(2);
      document.getElementById('table').hidden = false;
    };
  </script>
</body>
</html>

Off-by-one on last payment

Cap principalPaid at remaining balance — prevents negative balance row.

30-year table DOM size

360 rows is fine. For print, add show first 12 months toggle to reduce scroll on mobile.

Verify

$350k @ 6.5% / 30yr → payment ≈ $2,212 (compare bankrate.com) Extra $200 → fewer rows, lower final balance

Tags: CalculatorFinanceVanilla JSLead Gen