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
📈 Compound Interest Calculator
See how your investment grows over time
What You'll Learn
- Compound interest: the mathematics of exponential growth
- Compounding frequency: how monthly vs. quarterly vs. annual changes the result
- Monthly contributions: adding to the investment each month, not just once
- Simulation vs. formula: two approaches to computing compound interest
- Building tables with data objects: storing rows as objects before rendering
- Visual comparison: showing contributions vs. growth with a ratio bar
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.
Build the HTML Structure
<!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>
Style It with 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; }
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.
// ============================================================
// 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.
Write the JavaScript — Render & Events
// ============================================================
// 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.
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:
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.
Practice Challenges
🟢 Beginner
- Add a currency selector — USD, EUR, GBP, PKR, INR
- Add a print button for the yearly schedule
- Show interest as a % of final balance
🟡 Intermediate
- Add an inflation adjustment — show "real" (inflation-adjusted) value
- Add annual contribution increase — contribution grows by X% each year
- Add CSV download for the full schedule
🔴 Advanced
- Add a line chart (Chart.js) showing growth over time
- Add a scenario comparison — compare two different rates
- Add goal seeking — "to reach $1M, I need to contribute $X/month"
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().