What You'll Build

A compound interest calculator that projects how an investment grows over time. Enter your starting amount, monthly contribution, expected annual return, and duration — see the final balance, total contributions, and the interest earned. Plus a year-by-year schedule showing exactly how the money compounds.

  • Starting principal, monthly contribution, annual rate, years
  • Compounding frequency: monthly, quarterly, or annually
  • Final balance, total contributions, total interest earned
  • Visual bar showing contributions vs. interest
  • Full year-by-year growth table
  • Live updates as you type
▶ Live Preview — this is what you'll build

What You'll Learn

Key insight: Compound interest is exponential, not linear. Investing $10,000 with $200/month at 8% for 20 years grows to over $165,000 — but you only contributed about $58,000. The other $107,000 is pure compound growth.

1

Build the HTML Structure

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Compound Interest Calculator</title>
</head>
<body>
    <div class="card">
        <h1>Compound Interest Calculator</h1>
        <p class="sub">See how your investment grows over time</p>

        <div class="row">
            <div class="field">
                <label>Starting Amount ($)</label>
                <input type="number" id="principal" value="10000" step="500">
            </div>
            <div class="field">
                <label>Monthly Contribution ($)</label>
                <input type="number" id="monthly" value="200" step="25">
            </div>
        </div>

        <div class="row">
            <div class="field">
                <label>Annual Return (%)</label>
                <input type="number" id="rate" value="8" step="0.1">
            </div>
            <div class="field">
                <label>Years</label>
                <input type="number" id="years" value="20" step="1">
            </div>
        </div>

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

        <div class="result-box">
            <div class="label">Final Balance</div>
            <div class="num" id="final">$0</div>
        </div>

        <div class="stats">
            <div class="stat contrib">
                <div class="lbl">Total Contributed</div>
                <div class="val" id="contrib">$0</div>
            </div>
            <div class="stat interest">
                <div class="lbl">Total Interest</div>
                <div class="val" id="interest">$0</div>
            </div>
            <div class="stat total">
                <div class="lbl">Final Balance</div>
                <div class="val" id="finalStat">$0</div>
            </div>
        </div>

        <div class="bar">
            <div class="contrib" id="barContrib"></div>
            <div class="interest" id="barInterest"></div>
        </div>
        <div class="bar-labels">
            <span><span class="dot blue"></span>Contributions</span>
            <span><span class="dot green"></span>Interest</span>
        </div>

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

    <script src="script.js"></script>
</body>
</html>
2

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
}

.card {
    max-width: 540px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

.field { margin-bottom: 12px; }
label {
    display: block;
    font-size: 0.78rem;
    font-weight: 600;
    color: #334155;
    margin-bottom: 4px;
}
input[type="number"], select {
    width: 100%;
    padding: 11px 13px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 1rem;
    font-weight: 600;
    font-family: inherit;
    outline: none;
    transition: 0.2s;
}
input:focus, select:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

.row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

/* Result box — GREEN for growth */
.result-box {
    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
    border-radius: 12px;
    padding: 20px;
    text-align: center;
    color: #fff;
    margin: 16px 0;
}
.result-box .label {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    opacity: 0.9;
    font-weight: 700;
}
.result-box .num {
    font-size: 2rem;
    font-weight: 900;
    font-family: "Courier New", monospace;
    line-height: 1;
    margin-top: 6px;
}

.stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 16px;
}
.stat {
    background: #f8fafc;
    border-radius: 10px;
    padding: 12px;
    text-align: center;
}
.stat .lbl {
    font-size: 0.68rem;
    color: #64748b;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 700;
    margin-bottom: 4px;
}
.stat .val {
    font-size: 1.05rem;
    font-weight: 800;
    color: #1e293b;
    font-family: "Courier New", monospace;
}
.stat.contrib .val { color: #3b82f6; }
.stat.interest .val { color: #10b981; }
.stat.total { grid-column: 1 / -1; }
.stat.total .val { color: #059669; }

.bar {
    height: 12px;
    background: #e2e8f0;
    border-radius: 999px;
    overflow: hidden;
    display: flex;
    margin-bottom: 16px;
}
.bar .contrib { background: #3b82f6; }
.bar .interest { background: #10b981; }

.bar-labels {
    display: flex;
    justify-content: space-between;
    font-size: 0.72rem;
    color: #64748b;
    margin-bottom: 16px;
}
.bar-labels span { display: flex; align-items: center; gap: 6px; }
.bar-labels .dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
}
.bar-labels .dot.blue { background: #3b82f6; }
.bar-labels .dot.green { background: #10b981; }

.schedule {
    max-height: 320px;
    overflow-y: auto;
    border: 1px solid #e2e8f0;
    border-radius: 10px;
}
table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.78rem;
}
th {
    background: #f1f5f9;
    color: #334155;
    padding: 8px;
    text-align: right;
    font-weight: 700;
    position: sticky;
    top: 0;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
th:first-child { text-align: left; }
td {
    padding: 6px 8px;
    border-bottom: 1px solid #f1f5f9;
    text-align: right;
    font-family: "Courier New", monospace;
    color: #475569;
}
td:first-child {
    text-align: left;
    color: #0f172a;
    font-weight: 600;
}
td.growth { color: #059669; font-weight: 700; }
tr:last-child td { border-bottom: none; }
3

Write the JavaScript — The Growth Simulation

Instead of using a single compound interest formula, we simulate the growth month by month. This handles monthly contributions naturally and gives us the data for the yearly table.

JavaScript — Part 1: Simulation
// ============================================================
// GRAB DOM ELEMENTS
// ============================================================

var principalEl = document.getElementById("principal");
var monthlyEl = document.getElementById("monthly");
var rateEl = document.getElementById("rate");
var yearsEl = document.getElementById("years");
var freqEl = document.getElementById("freq");

// ============================================================
// CALC — the main growth simulation
// ============================================================
// Instead of using a single compound interest formula, we
// simulate growth month by month. This handles monthly
// contributions naturally.

function calc() {
    var P = parseFloat(principalEl.value) || 0;
    var monthly = parseFloat(monthlyEl.value) || 0;
    var annualRate = parseFloat(rateEl.value);
    var years = parseFloat(yearsEl.value);
    var n = parseInt(freqEl.value);

    if (!annualRate || !years || years <= 0) {
        return;
    }

    var r = annualRate / 100;
    var balance = P;
    var totalContrib = P;
    var yearlyRows = [];

    var months = Math.round(years * 12);

    // Convert annual rate + compounding freq into a monthly rate.
    // This lets us simulate contributions cleanly, regardless of
    // the compounding frequency the user chose.
    var ratePerMonth = Math.pow(1 + r / n, n / 12) - 1;

    for (var m = 1; m <= months; m++) {
        // Add monthly contribution first
        balance += monthly;
        totalContrib += monthly;

        // Then apply growth for the month
        balance *= (1 + ratePerMonth);

        // Every 12 months, capture a snapshot
        if (m % 12 === 0) {
            yearlyRows.push({
                year: m / 12,
                contributed: totalContrib,
                balance: balance,
                interest: balance - totalContrib
            });
        }
    }

    var finalBalance = balance;
    var totalInterest = finalBalance - totalContrib;

    // Update the display
    document.getElementById("final").textContent = fmt(finalBalance);
    document.getElementById("contrib").textContent = fmt(totalContrib);
    document.getElementById("interest").textContent = fmt(totalInterest);
    document.getElementById("finalStat").textContent = fmt(finalBalance);

    // Breakdown bar
    var pctContrib = (totalContrib / finalBalance) * 100;
    document.getElementById("barContrib").style.width = pctContrib + "%";
    document.getElementById("barInterest").style.width = (100 - pctContrib) + "%";

    renderSchedule(yearlyRows);
}

Why simulate month-by-month instead of using a formula? The compound interest formula (A = P(1 + r/n)^(nt)) works for a lump sum. But with monthly contributions, we need to iterate — each month's contribution grows for a different length of time. Simulating is simpler and more accurate.

4

Write the JavaScript — Render & Events

JavaScript — Part 2: Render
// ============================================================
// RENDER THE YEARLY SCHEDULE
// ============================================================
// The rows array contains objects like:
//   { year: 1, contributed: 12400, balance: 13400, interest: 1000 }
// We convert each into a <tr> row and join them all.

function renderSchedule(rows) {
    var schedule = document.getElementById("schedule");
    var html = ['<table><thead><tr>' +
                '<th>Year</th><th>Contributed</th>' +
                '<th>Interest</th><th>Balance</th>' +
                '</tr></thead><tbody>'];

    rows.forEach(function(r) {
        html.push('<tr>' +
                  '<td>Year ' + r.year + '</td>' +
                  '<td>' + fmt(r.contributed) + '</td>' +
                  '<td class="growth">' + fmt(r.interest) + '</td>' +
                  '<td>' + fmt(r.balance) + '</td>' +
                  '</tr>');
    });

    html.push('</tbody></table>');
    schedule.innerHTML = html.join("");
}

// ============================================================
// FORMAT NUMBERS AS CURRENCY
// ============================================================

function fmt(n) {
    if (!isFinite(n)) return "$0";
    return "$" + Math.round(n).toLocaleString();
}

// ============================================================
// EVENTS — live updates on any input change
// ============================================================

document.querySelectorAll("input, select").forEach(function(el) {
    el.addEventListener("input", calc);
    el.addEventListener("change", calc);
});

// Boot
calc();

Save and open index.html. Change any input — the final balance, contributions, interest, bar, and yearly table all update live.

5

Understand the Big Ideas

1. Exponential vs. linear growth

If you invested $10,000 and added $200/month at 0% interest, after 20 years you'd have $58,000. At 8%, you'd have $165,000. The extra $107,000 is compound growth — money making money.

Look at the yearly table. Year 1 interest is small. Year 20 interest is massive. This is the signature of exponential growth — the curve accelerates.

2. Compounding frequency matters

The more often interest compounds, the more you earn — but the difference is small. Over 20 years at 8%, monthly vs. annual compounding differs by less than 0.5%. The rate itself matters far more than the frequency.

3. Why we use a monthly rate

The user picks an annual rate and a compounding frequency, but contributions happen monthly. To make everything line up, we convert the annual rate to an effective monthly rate:

Formula
monthlyRate = (1 + annualRate / n)^(n / 12) - 1

Where n = compounding periods per year.
For monthly compounding:  n = 12
For quarterly compounding: n = 4
For annual compounding:    n = 1

This is the "effective monthly equivalent" of the annual rate, accounting for the compounding frequency. It's a subtle but important detail — using annualRate / 12 directly would be slightly wrong for non-monthly compounding.

4. Simulation vs. formula

Formula approach: A = P(1 + r/n)^(nt)
Simulation approach: iterate month by month

The formula is elegant but only handles lump sums. The simulation handles contributions, variable rates, and irregular events. In real financial apps, simulation wins.

6

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with these features — see the live Compound Interest Calculator.

Full Source Code

Common Mistakes & Fixes

❌ "Final balance is huge / unrealistic"

You're probably using the annual rate as a monthly rate. 8% annual is not 8% per month. Convert first with ratePerMonth = Math.pow(1 + r / n, n / 12) - 1.

❌ "Total interest is negative"

Your rate might be 0, or negative (bad investment). That's valid math, not a bug — you'd show interest as a cost. Check your input values.

❌ "The schedule doesn't match the final balance"

Make sure you're not double-adding the last contribution. The loop runs months times, and the last snapshot should equal the final balance.

❌ "Chart doesn't animate on change"

If you added Chart.js, you need to .update() the chart when data changes, not recreate it. Store the chart instance and call chart.update().

What to Build Next