What You'll Build

A loan EMI calculator that takes a principal amount, annual interest rate, and loan tenure, then computes the monthly EMI (Equated Monthly Installment), total interest paid, and a complete amortization schedule showing how each payment splits between principal and interest.

  • Three inputs: loan amount, annual interest rate, tenure in years
  • Instant EMI calculation using the standard formula
  • Total interest and total payment displayed
  • Visual breakdown bar (principal vs. interest)
  • Full amortization schedule table (all months)
  • Live updates as you type
▶ Live Preview — this is what you'll build

What You'll Learn

The formula: EMI = [P × r × (1+r)^n] ÷ [(1+r)^n − 1]
P = principal, r = monthly interest rate (annual ÷ 12 ÷ 100), n = total months

1

Build the HTML Structure

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Loan EMI Calculator</title>
</head>
<body>
    <div class="card">
        <h1>Loan EMI Calculator</h1>
        <p class="sub">Calculate monthly payments and total interest</p>

        <div class="field">
            <label>Loan Amount ($)</label>
            <input type="number" id="amount" value="200000" step="1000">
        </div>
        <div class="field">
            <label>Annual Interest Rate (%)</label>
            <input type="number" id="rate" value="6.5" step="0.1">
        </div>
        <div class="field">
            <label>Loan Tenure (years)</label>
            <input type="number" id="years" value="20" step="1">
        </div>

        <div class="result-box">
            <div class="label">Monthly EMI</div>
            <div class="num" id="emi">$0</div>
        </div>

        <div class="stats">
            <div class="stat">
                <div class="lbl">Principal</div>
                <div class="val" id="principal">$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">Total Payment</div>
                <div class="val" id="total">$0</div>
            </div>
        </div>

        <div class="bar">
            <div class="principal" id="barPrincipal"></div>
            <div class="interest" id="barInterest"></div>
        </div>
        <div class="bar-labels">
            <span><span class="dot blue"></span>Principal</span>
            <span><span class="dot red"></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: 520px;
    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"] {
    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 {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

.result-box {
    background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 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.1rem;
    font-weight: 800;
    color: #1e293b;
    font-family: "Courier New", monospace;
}
.stat.interest .val { color: #ef4444; }
.stat.total .val { color: #1d4ed8; }
.stat.total { grid-column: 1 / -1; }

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

.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.red { background: #ef4444; }

.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;
}
tr:last-child td { border-bottom: none; }
3

Write the JavaScript — EMI Formula

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

var amountEl = document.getElementById("amount");
var rateEl = document.getElementById("rate");
var yearsEl = document.getElementById("years");

// ============================================================
// CALCULATE EMI
// ============================================================
// Formula: EMI = [P x r x (1+r)^n] / [(1+r)^n - 1]
//   P = principal (loan amount)
//   r = monthly interest rate = (annual / 12) / 100
//   n = total months = years x 12

function calc() {
    var P = parseFloat(amountEl.value);
    var annualRate = parseFloat(rateEl.value);
    var years = parseFloat(yearsEl.value);

    // Guard clause: ignore invalid input
    if (!P || !annualRate || !years || P <= 0 || annualRate <= 0 || years <= 0) {
        return;
    }

    var r = annualRate / 12 / 100;
    var n = years * 12;

    // The magic formula — compound interest solved for a fixed payment
    var emi = (P * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);

    var totalPayment = emi * n;
    var totalInterest = totalPayment - P;

    // Write results
    document.getElementById("emi").textContent = fmt(emi);
    document.getElementById("principal").textContent = fmt(P);
    document.getElementById("interest").textContent = fmt(totalInterest);
    document.getElementById("total").textContent = fmt(totalPayment);

    // Update the principal/interest bar
    var pct = (P / totalPayment) * 100;
    document.getElementById("barPrincipal").style.width = pct + "%";
    document.getElementById("barInterest").style.width = (100 - pct) + "%";

    // Render the amortization schedule
    renderSchedule(P, r, emi, n);
}

Why this formula works: It comes from solving the compound interest equation for a fixed monthly payment. If you lent $200,000 at 6.5%/year for 20 years, the bank calculates a payment where the present value of all future payments equals $200,000. That payment is $1,491/month.

4

Write the JavaScript — Amortization & Events

JavaScript — Part 2: Schedule
// ============================================================
// AMORTIZATION SCHEDULE
// ============================================================
// Each month, part of the payment goes to interest and part
// goes to principal. Early on, most is interest. Later, most
// is principal. This function simulates every month.

function renderSchedule(P, r, emi, n) {
    var schedule = document.getElementById("schedule");
    var rows = [];

    rows.push('<table><thead><tr>' +
              '<th>Month</th><th>Principal</th>' +
              '<th>Interest</th><th>Balance</th>' +
              '</tr></thead><tbody>');

    var balance = P;

    for (var i = 1; i <= n; i++) {
        var interestPart = balance * r;              // interest this month
        var principalPart = emi - interestPart;      // principal this month
        balance = balance - principalPart;           // remaining balance

        if (balance < 0) balance = 0;                 // clean final row

        rows.push('<tr>' +
                  '<td>' + i + '</td>' +
                  '<td>' + fmt(principalPart) + '</td>' +
                  '<td>' + fmt(interestPart) + '</td>' +
                  '<td>' + fmt(balance) + '</td>' +
                  '</tr>');
    }

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

// ============================================================
// NUMBER FORMATTING — money format
// ============================================================

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

// ============================================================
// LIVE UPDATES
// ============================================================

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

// Boot
calc();

Save and open index.html. Change the loan amount, rate, or tenure — the EMI, totals, breakdown bar, and full schedule all update live.

5

Understand the Big Ideas

1. Financial formulas in code

The EMI formula is a classic compound interest calculation. The Math.pow(1 + r, n) term raises the monthly rate to the power of the number of months. This grows exponentially, which is why 20 years of payments is much more than 20 × 12 × the principal.

2. Amortization — the compounding in reverse

In month 1, you pay interest on the full $200,000 balance. In month 240, you pay interest on just a few hundred dollars. The schedule makes this visible — early payments are mostly interest, late payments are mostly principal.

This is why overpaying early in a loan saves far more than overpaying late. Every extra dollar you put toward principal in month 1 saves you interest for the next 239 months.

3. Building tables from loops

We never write a single <tr> by hand. The loop builds an array of strings, then join("") merges them into one HTML string that gets injected with innerHTML. This is the standard pattern for rendering lists of any size.

Performance note: For 240 rows this is instant. For 100,000 rows, you'd want a proper virtual-DOM library — but that's a problem for another day. For real loan schedules, this approach is perfect.

4. Money formatting

Math.round(n).toLocaleString() rounds to whole dollars and adds thousands separators using the user's locale. In the US, this gives 1,491. In Germany, 1.491. Same code, right output everywhere.

6

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

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

Full Source Code

Common Mistakes & Fixes

❌ "EMI is way too high"

You're probably using the annual rate as the monthly rate. Divide by 12 first, then by 100. So 6.5% annual = 6.5 / 12 / 100 = 0.00542 monthly.

❌ "Schedule shows negative balances"

Floating-point rounding can push the last balance slightly below zero. The if (balance < 0) balance = 0 line cleans this up.

❌ "Infinity or NaN in the result"

Check for zero in the rate or years. The guard clause at the top of calc() handles this — make sure it's not been removed.

❌ "Page is slow with 30-year loans"

30 years = 360 rows. Should be instant. If it's slow, you're probably re-rendering the whole table on every keystroke instead of on change. Consider debouncing the input.

What to Build Next