Build an Amortization Calculator: The "Debt Snowball" Logic
A mortgage calculator tells you the monthly payment. An Amortization Calculator tells you the story of the loan.
It shows exactly how much of your hard-earned money goes to the bank (interest) vs. your actual debt (principal) every single month for 30 years.
In this guide, we'll go beyond simple formulas and build the Schedule Generator—a loop that calculates the loan's life month by month.
Step 1: The Core variable (Remaining Balance)
Unlike a simple mortgage calculator, we need to track state over time.
The most important variable is balance. It starts equal to the Loan Amount and must reach $0 by the end.
let balance = 200000; // Loan Amount
const monthlyRate = 0.005; // 6% annual / 12 months
const monthlyPayment = 1199.10; // Calculated via formula
Step 2: The Logic Loop 🔄
We need to run a calculation for every month of the loan (e.g., 360 months for a 30-year mortgage).
We use a for loop.
for (let month = 1; month <= 360; month++) {
// Magic happens here
}
Step 3: Calculating Interest (The Bank's Cut) 🏦
Every month, the bank charges you interest on the current balance. This is why payments at the start are mostly interest!
const interestPayment = balance * monthlyRate;
Step 4: Calculating Principal (Your Equity) 🏠
Whatever is left of your payment after interest goes to paying down the debt.
const principalPayment = monthlyPayment - interestPayment;
Step 5: Updating the Balance 📉
Now we lower the debt. This "new" balance is used for next month's interest calculation. This is where the "Snowball" effect happens: simpler debt = less interest = more principal paid next time.
balance = balance - principalPayment;
Step 6: Handling Extra Payments 🚀
What if you pay an extra $100? We simply subtract it from the balance directly. This skips interest entirely!
const totalPrincipal = principalPayment + extraPayment;
balance = balance - totalPrincipal;
Step 7: Structuring the Data (Array of Objects)
We don't just want to console.log it. We want an array of objects to render a table.
const schedule = [];
// ... inside loop ...
schedule.push({
month: month,
interest: interestPayment,
principal: principalPayment,
remaining: balance
});
Step 8: Rendering the Table (HTML)
We loop through our data array to create rows.
schedule.map(row => (
<tr>
<td>Month {row.month}</td>
<td>${row.interest.toFixed(2)}</td>
<td>${row.principal.toFixed(2)}</td>
<td>${row.remaining.toFixed(2)}</td>
</tr>
))
Step 9: Precision Issues (Floating Point Math) 🧮
Computers are bad at decimals (0.1 + 0.2 = 0.300000004).
To prevent ugly numbers, always use .toFixed(2) for display, but keep raw numbers for math until the very end.
Step 10: The Full Code (Schedule Generator) 💻
Here is a complete, working React component that generates a full Amortization Schedule.
import React, { useState } from 'react';
export default function AmortizationCalculator() {
const [loan, setLoan] = useState(200000);
const [rate, setRate] = useState(6);
const [years, setYears] = useState(30);
const [schedule, setSchedule] = useState([]);
const generateSchedule = () => {
const months = years * 12;
const monthlyRate = rate / 100 / 12;
// Standard Mortgage Formula for Payment
const payment = (loan * monthlyRate * Math.pow(1 + monthlyRate, months)) /
(Math.pow(1 + monthlyRate, months) - 1);
let balance = loan;
const newSchedule = [];
for (let i = 1; i <= months; i++) {
const interest = balance * monthlyRate;
const principal = payment - interest;
// Prevent negative balance at the very end due to rounding
if (balance < principal) {
balance = 0;
} else {
balance -= principal;
}
newSchedule.push({
month: i,
payment: payment,
interest: interest,
principal: principal,
balance: balance
});
if (balance <= 0) break;
}
setSchedule(newSchedule);
};
<ArticleAd />
return (
<div className="p-6 bg-white rounded-xl shadow-lg max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-4">Amortization Schedule</h2>
<div className="grid grid-cols-3 gap-4 mb-4">
<input type="number" value={loan} onChange={e => setLoan(Number(e.target.value))} placeholder="Loan Amount" className="border p-2 rounded" />
<input type="number" value={rate} onChange={e => setRate(Number(e.target.value))} placeholder="Rate %" className="border p-2 rounded" />
<input type="number" value={years} onChange={e => setYears(Number(e.target.value))} placeholder="Years" className="border p-2 rounded" />
</div>
<button onClick={generateSchedule} className="w-full bg-blue-600 text-white p-3 rounded font-bold hover:bg-blue-700 transition">
Calculate Schedule
</button>
{schedule.length > 0 && (
<div className="mt-6 overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-gray-100 uppercase">
<tr>
<th className="px-4 py-2">Month</th>
<th className="px-4 py-2">Payment</th>
<th className="px-4 py-2">Principal</th>
<th className="px-4 py-2">Interest</th>
<th className="px-4 py-2">Balance</th>
</tr>
</thead>
<tbody>
{schedule.map((row) => (
<tr key={row.month} className="border-b">
<td className="px-4 py-2">{row.month}</td>
<td className="px-4 py-2 font-mono">${row.payment.toFixed(2)}</td>
<td className="px-4 py-2 font-mono text-green-600">${row.principal.toFixed(2)}</td>
<td className="px-4 py-2 font-mono text-red-600">${row.interest.toFixed(2)}</td>
<td className="px-4 py-2 font-mono text-gray-500">${row.balance.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}