DEV Community

Profiterole
Profiterole

Posted on

5 Finance Calculators You Can Build with Pure HTML/CSS/JS (No Framework Needed)

Building finance tools doesn't require React, Vue, or any framework. Pure HTML, CSS, and vanilla JavaScript is all you need to create genuinely useful calculators that run entirely in the browser — no backend, no build step, no npm install.

In this post I'll walk through 3 complete calculators you can build today, then point you to a collection of 155+ more.


Why Vanilla JS for Finance Calculators?

Finance calculators are a perfect use case for no-framework development:

  • Stateless: A compound interest calc just takes inputs and returns outputs. No global state needed.
  • Math-heavy, not UI-heavy: The logic is formulas, not complex UI interactions.
  • Fast to ship: No bundler configuration. Open a .html file, start coding.
  • SEO-friendly: Static HTML indexes and renders immediately.

Let's build three of them.


1. Compound Interest Calculator

The formula: A = P(1 + r/n)^(nt)

Where:

  • P = principal
  • r = annual rate (decimal)
  • n = compounding frequency per year
  • t = time in years
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Compound Interest Calculator</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; }
    label { display: block; margin-top: 16px; font-weight: 600; }
    input, select { width: 100%; padding: 8px; margin-top: 4px; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; }
    button { margin-top: 20px; width: 100%; padding: 10px; background: #2563eb; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
    #result { margin-top: 20px; padding: 16px; background: #f0f9ff; border-radius: 8px; }
  </style>
</head>
<body>
  <h1>Compound Interest</h1>

  <label>Principal ($)</label>
  <input type="number" id="principal" value="10000">

  <label>Annual Rate (%)</label>
  <input type="number" id="rate" value="7" step="0.1">

  <label>Years</label>
  <input type="number" id="years" value="10">

  <label>Compounding Frequency</label>
  <select id="freq">
    <option value="1">Annually</option>
    <option value="4">Quarterly</option>
    <option value="12" selected>Monthly</option>
    <option value="365">Daily</option>
  </select>

  <button onclick="calculate()">Calculate</button>
  <div id="result" hidden></div>

  <script>
    function calculate() {
      const P = parseFloat(document.getElementById('principal').value);
      const r = parseFloat(document.getElementById('rate').value) / 100;
      const t = parseFloat(document.getElementById('years').value);
      const n = parseInt(document.getElementById('freq').value);

      const A = P * Math.pow(1 + r / n, n * t);
      const interest = A - P;

      const fmt = v => v.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
      const result = document.getElementById('result');
      result.hidden = false;
      result.innerHTML = `
        <strong>Final Balance:</strong> ${fmt(A)}<br>
        <strong>Total Interest:</strong> ${fmt(interest)}<br>
        <strong>Return:</strong> ${((interest / P) * 100).toFixed(1)}%
      `;
    }
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

That's it — 60 lines, no dependencies.


2. Debt Snowball Calculator

The snowball method: pay minimums on all debts, throw extra money at the smallest balance first. As each debt is paid off, roll that payment into the next one.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Debt Snowball</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
    table { width: 100%; border-collapse: collapse; margin-top: 16px; }
    th, td { padding: 8px; border: 1px solid #ddd; text-align: right; }
    th:first-child, td:first-child { text-align: left; }
    button { margin: 8px 4px 0; padding: 8px 16px; cursor: pointer; }
    #plan { margin-top: 24px; }
    .debt-row { background: #f8f8f8; padding: 8px; margin: 8px 0; border-radius: 4px; }
  </style>
</head>
<body>
  <h1>Debt Snowball</h1>
  <label>Extra monthly payment ($): <input type="number" id="extra" value="200"></label>

  <table>
    <thead><tr><th>Debt</th><th>Balance ($)</th><th>Min Payment ($)</th><th>Rate (%)</th><th></th></tr></thead>
    <tbody id="debts">
      <tr>
        <td><input value="Credit Card" style="width:100%"></td>
        <td><input type="number" value="3500" style="width:80px"></td>
        <td><input type="number" value="75" style="width:70px"></td>
        <td><input type="number" value="19.9" step="0.1" style="width:60px"></td>
        <td><button onclick="removeRow(this)"></button></td>
      </tr>
      <tr>
        <td><input value="Car Loan" style="width:100%"></td>
        <td><input type="number" value="8200" style="width:80px"></td>
        <td><input type="number" value="220" style="width:70px"></td>
        <td><input type="number" value="6.5" step="0.1" style="width:60px"></td>
        <td><button onclick="removeRow(this)"></button></td>
      </tr>
    </tbody>
  </table>

  <button onclick="addRow()">+ Add Debt</button>
  <button onclick="runSnowball()" style="background:#2563eb;color:white">Calculate Snowball</button>
  <div id="plan"></div>

  <script>
    function addRow() {
      const tbody = document.getElementById('debts');
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td><input value="New Debt" style="width:100%"></td>
        <td><input type="number" value="1000" style="width:80px"></td>
        <td><input type="number" value="25" style="width:70px"></td>
        <td><input type="number" value="12" step="0.1" style="width:60px"></td>
        <td><button onclick="removeRow(this)">✕</button></td>`;
      tbody.appendChild(tr);
    }

    function removeRow(btn) { btn.closest('tr').remove(); }

    function runSnowball() {
      const rows = document.querySelectorAll('#debts tr');
      let debts = Array.from(rows).map(r => {
        const inputs = r.querySelectorAll('input');
        return {
          name: inputs[0].value,
          balance: parseFloat(inputs[1].value),
          min: parseFloat(inputs[2].value),
          rate: parseFloat(inputs[3].value) / 100 / 12
        };
      }).filter(d => d.balance > 0);

      // Sort by balance ascending (snowball order)
      debts.sort((a, b) => a.balance - b.balance);

      let extra = parseFloat(document.getElementById('extra').value) || 0;
      let month = 0;
      let totalInterest = 0;

      while (debts.some(d => d.balance > 0) && month < 600) {
        month++;
        let available = extra;

        for (let d of debts) {
          if (d.balance <= 0) { available += d.min; continue; }
          const interest = d.balance * d.rate;
          totalInterest += interest;
          d.balance = d.balance + interest - d.min;
          if (d.balance < 0) { available += Math.abs(d.balance); d.balance = 0; }
        }

        // Apply extra to first non-zero debt
        const target = debts.find(d => d.balance > 0);
        if (target) {
          target.balance = Math.max(0, target.balance - available);
        }
      }

      const fmt = v => '$' + v.toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0});
      document.getElementById('plan').innerHTML = `
        <div class="debt-row">
          <strong>Debt-free in:</strong> ${month} months (${(month/12).toFixed(1)} years)<br>
          <strong>Total Interest Paid:</strong> ${fmt(totalInterest)}
        </div>`;
    }
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The key insight: when a debt hits zero, its minimum payment becomes available extra cash for the next target.


3. Savings Goal Calculator

"How much do I need to save each month to reach my goal?"

The formula for required monthly payment:
PMT = FV × r / ((1 + r)^n − 1)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Savings Goal Calculator</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 480px; margin: 40px auto; padding: 0 20px; }
    .field { margin: 16px 0; }
    label { display: block; font-weight: 600; margin-bottom: 4px; }
    input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; }
    .result { margin-top: 24px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    .card { background: #f0f9ff; border-radius: 8px; padding: 16px; }
    .card .value { font-size: 1.5rem; font-weight: 700; color: #1d4ed8; }
    .card .label { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
  </style>
</head>
<body>
  <h1>Savings Goal</h1>

  <div class="field">
    <label>Target Amount ($)</label>
    <input type="number" id="goal" value="20000" oninput="calc()">
  </div>
  <div class="field">
    <label>Current Savings ($)</label>
    <input type="number" id="current" value="2000" oninput="calc()">
  </div>
  <div class="field">
    <label>Annual Interest Rate (%)</label>
    <input type="number" id="rate" value="4.5" step="0.1" oninput="calc()">
  </div>
  <div class="field">
    <label>Time to Goal (months)</label>
    <input type="number" id="months" value="24" oninput="calc()">
  </div>

  <div class="result" id="result"></div>

  <script>
    function calc() {
      const FV = parseFloat(document.getElementById('goal').value) || 0;
      const PV = parseFloat(document.getElementById('current').value) || 0;
      const annualRate = parseFloat(document.getElementById('rate').value) || 0;
      const n = parseInt(document.getElementById('months').value) || 1;
      const r = annualRate / 100 / 12;

      const needed = FV - PV;
      let monthly;

      if (r === 0) {
        monthly = needed / n;
      } else {
        // FV of annuity formula, solving for PMT
        monthly = needed * r / (Math.pow(1 + r, n) - 1);
        // Subtract growth of existing savings
        const pvGrowth = PV * Math.pow(1 + r, n);
        monthly = (FV - pvGrowth) * r / (Math.pow(1 + r, n) - 1);
      }

      const fmt = v => '$' + Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
      const totalContributions = Math.max(0, monthly) * n;
      const totalInterest = FV - PV - totalContributions;

      document.getElementById('result').innerHTML = `
        <div class="card">
          <div class="value">${fmt(Math.max(0, monthly))}/mo</div>
          <div class="label">Required monthly savings</div>
        </div>
        <div class="card">
          <div class="value">${fmt(totalInterest)}</div>
          <div class="label">Interest earned</div>
        </div>
      `;
    }

    calc(); // Run on load
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note the oninput="calc()" — no button needed. The result updates live as you type.


Key Patterns Worth Noting

1. oninput for live updates
Instead of a submit button, attach oninput to every input. Finance calculators feel much better when results update in real time.

2. toLocaleString for formatting

value.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
// → $12,345.67
Enter fullscreen mode Exit fullscreen mode

3. Guard against division by zero

const r = annualRate / 100 / 12;
if (r === 0) {
  monthly = needed / n;  // Simple division
} else {
  monthly = /* compound formula */;
}
Enter fullscreen mode Exit fullscreen mode

4. Keep state in the DOM
For simple calculators, you don't need useState or a store. Read directly from document.getElementById('input').value when you need the value.


What Else Can You Build?

These three cover the basics, but personal finance has dozens of useful calculators:

  • FIRE Number — how much you need to retire (25× rule, 4% withdrawal)
  • Rent vs Buy — break-even analysis with opportunity cost
  • Emergency Fund — months of runway at current spend
  • Net Worth Tracker — assets minus liabilities snapshot
  • Credit Card Payoff — avalanche vs snowball comparison
  • Mortgage Amortization — full payment schedule table

I've built 155+ of these calculators as a free tool — check out the full collection at Profiterole Finance Calculators.

No login, no ads, no tracking. Just open the page and use them.


Wrapping Up

Finance calculators are one of the best types of tools to build without a framework:

  • Pure math in, formatted output out
  • No server needed
  • Fast to build and easy to maintain
  • Users actually find them useful

Start with the compound interest one — it's 60 lines and immediately useful. Then extend it: add monthly contributions, visualize with a chart, add a comparison mode.

What finance calculator would you want to see next? Drop it in the comments.

Top comments (0)