<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>EMI Calculator</title>
<script src=”https://cdn.tailwindcss.com”></script>
<script src=”https://cdn.jsdelivr.net/npm/chart.js”></script>
<link rel=”preconnect” href=”https://fonts.googleapis.com”>
<link rel=”preconnect” href=”https://fonts.gstatic.com” crossorigin>
<link href=”https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap” rel=”stylesheet”>
<style>
body {
font-family: ‘Inter’, sans-serif;
}
/* Custom styles for sliders */
input[type=”range”] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
background: #e5e7eb; /* gray-200 */
border-radius: 9999px;
outline: none;
opacity: 0.9;
transition: opacity .2s;
}
input[type=”range”]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #16a34a; /* green-600 */
cursor: pointer;
border-radius: 50%;
border: 2px solid white;
}
input[type=”range”]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #16a34a; /* green-600 */
cursor: pointer;
border-radius: 50%;
border: 2px solid white;
}
.monthly-details td {
padding: 0;
background-color: #f9fafb; /* gray-50 */
}
.monthly-details .inner-table {
margin: 1rem;
width: calc(100% – 2rem);
}
</style>
</head>
<body class=”bg-gray-50 text-gray-800″>
<div class=”container mx-auto p-4 md:p-8 max-w-7xl”>
<h1 class=”text-3xl md:text-4xl font-bold text-center text-gray-900 mb-2″>EMI Calculator</h1>
<p class=”text-center text-gray-500 mb-8 md:mb-12″>Calculate your loan EMI (Equated Monthly Instalment) easily.</p>
<div class=”grid grid-cols-1 lg:grid-cols-5 gap-8″>
<!– Left Side: Inputs –>
<div class=”lg:col-span-3 bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-gray-200″>
<div class=”space-y-8″>
<!– Loan Amount –>
<div>
<div class=”flex justify-between items-center mb-2″>
<label for=”amount” class=”font-medium text-gray-700″>Loan Amount</label>
<div class=”relative”>
<span class=”absolute left-3 top-1/2 -translate-y-1/2 text-gray-500″>₹</span>
<input type=”text” id=”amountInput” class=”w-40 pl-7 pr-2 py-2 text-right font-semibold text-lg border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500″>
</div>
</div>
<input type=”range” id=”amountSlider” min=”100000″ max=”50000000″ step=”10000″ value=”2500000″>
<div class=”flex justify-between text-sm text-gray-500 mt-1″>
<span>₹1 Lakh</span>
<span>₹5 Cr</span>
</div>
</div>
<!– Interest Rate –>
<div>
<div class=”flex justify-between items-center mb-2″>
<label for=”interest” class=”font-medium text-gray-700″>Interest Rate</label>
<div class=”relative”>
<input type=”text” id=”interestInput” class=”w-28 pr-7 py-2 text-right font-semibold text-lg border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500″>
<span class=”absolute right-3 top-1/2 -translate-y-1/2 text-gray-500″>%</span>
</div>
</div>
<input type=”range” id=”interestSlider” min=”5″ max=”20″ step=”0.05″ value=”8.5″>
<div class=”flex justify-between text-sm text-gray-500 mt-1″>
<span>5%</span>
<span>20%</span>
</div>
</div>
<!– Loan Tenure –>
<div>
<div class=”flex justify-between items-center mb-2″>
<label for=”tenure” class=”font-medium text-gray-700″>Loan Tenure</label>
<div class=”relative”>
<input type=”text” id=”tenureInput” class=”w-28 pr-12 py-2 text-right font-semibold text-lg border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500″>
<span class=”absolute right-3 top-1/2 -translate-y-1/2 text-gray-500″>Yr</span>
</div>
</div>
<input type=”range” id=”tenureSlider” min=”1″ max=”30″ step=”1″ value=”20″>
<div class=”flex justify-between text-sm text-gray-500 mt-1″>
<span>1 Yr</span>
<span>30 Yrs</span>
</div>
</div>
</div>
</div>
<!– Right Side: Outputs & Chart –>
<div class=”lg:col-span-2 bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-gray-200 flex flex-col justify-center items-center”>
<div class=”w-full max-w-xs mx-auto”>
<canvas id=”emiChart”></canvas>
</div>
<div class=”w-full mt-6 text-center”>
<div class=”mb-4″>
<p class=”text-gray-500 text-lg”>Monthly EMI</p>
<p id=”monthlyEmi” class=”text-3xl md:text-4xl font-bold text-gray-900″>₹0</p>
</div>
<div class=”flex justify-between text-base”>
<div class=”text-left”>
<p class=”text-gray-500″>Principal Amount</p>
<p id=”totalPrincipal” class=”font-semibold text-lg”>₹0</p>
</div>
<div class=”text-right”>
<p class=”text-gray-500″>Total Interest</p>
<p id=”totalInterest” class=”font-semibold text-lg”>₹0</p>
</div>
</div>
<div class=”border-t my-3″></div>
<div class=”flex justify-between text-base”>
<div class=”text-left”>
<p class=”text-gray-500″>Total Payment</p>
<p id=”totalPayment” class=”font-semibold text-lg”>₹0</p>
</div>
</div>
</div>
</div>
</div>
<!– Amortization Schedule –>
<div class=”mt-8 bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-gray-200″>
<h2 class=”text-2xl font-bold text-gray-900 mb-4″>Amortization Details</h2>
<div class=”overflow-x-auto”>
<table class=”min-w-full divide-y divide-gray-200″>
<thead class=”bg-gray-50″>
<tr>
<th scope=”col” class=”px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider”>Year</th>
<th scope=”col” class=”px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider”>Principal</th>
<th scope=”col” class=”px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider”>Interest</th>
<th scope=”col” class=”px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider”>Total Payment</th>
<th scope=”col” class=”px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider”>Balance</th>
</tr>
</thead>
<tbody id=”scheduleBody” class=”bg-white divide-y divide-gray-200″>
<!– Rows will be injected by JavaScript –>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener(‘DOMContentLoaded’, function () {
// DOM Elements
const amountSlider = document.getElementById(‘amountSlider’);
const amountInput = document.getElementById(‘amountInput’);
const interestSlider = document.getElementById(‘interestSlider’);
const interestInput = document.getElementById(‘interestInput’);
const tenureSlider = document.getElementById(‘tenureSlider’);
const tenureInput = document.getElementById(‘tenureInput’);
const monthlyEmiEl = document.getElementById(‘monthlyEmi’);
const totalPrincipalEl = document.getElementById(‘totalPrincipal’);
const totalInterestEl = document.getElementById(‘totalInterest’);
const totalPaymentEl = document.getElementById(‘totalPayment’);
const scheduleBody = document.getElementById(‘scheduleBody’);
let emiChart;
// Format numbers with commas
const formatCurrency = (num) => `₹${Math.round(num).toLocaleString(‘en-IN’)}`;
const formatInputCurrency = (num) => Math.round(num).toLocaleString(‘en-IN’, { useGrouping: false });
// Chart Initialization
const ctx = document.getElementById(’emiChart’).getContext(‘2d’);
function createOrUpdateChart(principal, interest) {
const data = {
labels: [‘Principal Amount’, ‘Total Interest’],
datasets: [{
data: [principal, interest],
backgroundColor: [‘#16a34a’, ‘#dcfce7’], // green-600, green-100
borderColor: [‘#ffffff’, ‘#ffffff’],
borderWidth: 2,
hoverOffset: 4
}]
};
if (emiChart) {
emiChart.data = data;
emiChart.update();
} else {
emiChart = new Chart(ctx, {
type: ‘doughnut’,
data: data,
options: {
responsive: true,
cutout: ‘70%’,
plugins: {
legend: {
position: ‘bottom’,
labels: {
usePointStyle: true,
pointStyle: ‘circle’,
padding: 20,
boxWidth: 8,
boxHeight: 8
}
},
tooltip: {
callbacks: {
label: function (context) {
let label = context.label || ”;
if (label) {
label += ‘: ‘;
}
if (context.parsed !== null) {
label += formatCurrency(context.parsed);
}
return label;
}
}
}
}
}
});
}
}
// Main Calculation Function
function calculate() {
const p = parseFloat(amountSlider.value);
const rAnnual = parseFloat(interestSlider.value);
const tYears = parseInt(tenureSlider.value, 10);
if (isNaN(p) || isNaN(rAnnual) || isNaN(tYears) || p <= 0 || rAnnual <= 0 || tYears <= 0) {
return;
}
const rMonthly = rAnnual / 12 / 100;
const nMonths = tYears * 12;
const emi = (p * rMonthly * Math.pow(1 + rMonthly, nMonths)) / (Math.pow(1 + rMonthly, nMonths) – 1);
const totalPayment = emi * nMonths;
const totalInterest = totalPayment – p;
// Update summary display
monthlyEmiEl.textContent = formatCurrency(emi);
totalPrincipalEl.textContent = formatCurrency(p);
totalInterestEl.textContent = formatCurrency(totalInterest);
totalPaymentEl.textContent = formatCurrency(totalPayment);
// Update Chart
createOrUpdateChart(p, totalInterest);
// Generate and display schedule
generateSchedule(p, rAnnual, tYears, emi);
}
// Generate Collapsible Amortization Schedule, grouped by calendar year
function generateSchedule(p, rAnnual, tYears, emi) {
scheduleBody.innerHTML = ”;
let balance = p;
const rMonthly = rAnnual / 12 / 100;
const nMonths = tYears * 12;
const monthNames = [“Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec”];
const startDate = new Date();
const startMonth = startDate.getMonth(); // 0-11
const startYear = startDate.getFullYear();
// 1. Create a flat list of all monthly payments
const allPayments = [];
for (let i = 0; i < nMonths; i++) {
const interestForMonth = balance * rMonthly;
const principalForMonth = emi – interestForMonth;
balance -= principalForMonth;
const monthOffset = i;
const displayMonthIndex = (startMonth + monthOffset) % 12;
const yearOffset = Math.floor((startMonth + monthOffset) / 12);
const displayYear = startYear + yearOffset;
const displayMonthName = monthNames[displayMonthIndex];
allPayments.push({
monthName: displayMonthName,
year: displayYear,
principal: principalForMonth,
interest: interestForMonth,
total: emi,
balance: balance < 0 ? 0 : balance,
});
}
// 2. Group payments by calendar year
const groupedByYear = allPayments.reduce((acc, payment) => {
const year = payment.year;
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(payment);
return acc;
}, {});
// 3. Iterate through grouped data and build HTML
for (const year in groupedByYear) {
const yearPayments = groupedByYear[year];
const yearlyPrincipal = yearPayments.reduce((sum, p) => sum + p.principal, 0);
const yearlyInterest = yearPayments.reduce((sum, p) => sum + p.interest, 0);
const yearlyTotalPayment = yearlyPrincipal + yearlyInterest;
const endOfYearBalance = yearPayments[yearPayments.length – 1].balance;
let monthlyRowsHtml = ”;
yearPayments.forEach(p => {
monthlyRowsHtml += `
<tr class=”hover:bg-white”>
<td class=”px-6 py-2 text-sm text-gray-700 font-medium”>${p.monthName}</td>
<td class=”px-6 py-2 text-sm text-gray-500 text-right”>${formatCurrency(p.principal)}</td>
<td class=”px-6 py-2 text-sm text-gray-500 text-right”>${formatCurrency(p.interest)}</td>
<td class=”px-6 py-2 text-sm text-gray-500 text-right”>${formatCurrency(p.total)}</td>
<td class=”px-6 py-2 text-sm text-gray-500 text-right”>${formatCurrency(p.balance)}</td>
</tr>
`;
});
const yearRowHtml = `
<tr class=”year-summary-row cursor-pointer hover:bg-gray-50″ data-year=”${year}”>
<td class=”px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900″>
<span class=”expand-icon inline-block w-4 mr-2 text-lg font-bold text-green-600 transition-transform transform”>+</span>
${year}
</td>
<td class=”px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right font-medium”>${formatCurrency(yearlyPrincipal)}</td>
<td class=”px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right font-medium”>${formatCurrency(yearlyInterest)}</td>
<td class=”px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right font-medium”>${formatCurrency(yearlyTotalPayment)}</td>
<td class=”px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right font-medium”>${formatCurrency(endOfYearBalance)}</td>
</tr>
`;
const monthlyDetailsRowHtml = `
<tr class=”monthly-details hidden” data-details-for-year=”${year}”>
<td colspan=”5″>
<div class=”inner-table”>
<table class=”min-w-full”>
<thead class=”bg-gray-200″>
<tr>
<th class=”px-6 py-2 text-left text-xs font-medium text-gray-600 uppercase tracking-wider”>Month</th>
<th class=”px-6 py-2 text-right text-xs font-medium text-gray-600 uppercase tracking-wider”>Principal Paid</th>
<th class=”px-6 py-2 text-right text-xs font-medium text-gray-600 uppercase tracking-wider”>Interest Paid</th>
<th class=”px-6 py-2 text-right text-xs font-medium text-gray-600 uppercase tracking-wider”>Total Payment</th>
<th class=”px-6 py-2 text-right text-xs font-medium text-gray-600 uppercase tracking-wider”>Ending Balance</th>
</tr>
</thead>
<tbody class=”divide-y divide-gray-300″>
${monthlyRowsHtml}
</tbody>
</table>
</div>
</td>
</tr>
`;
scheduleBody.innerHTML += yearRowHtml + monthlyDetailsRowHtml;
}
}
// Sync Sliders and Inputs
function syncInputs(slider, input, isCurrency) {
slider.addEventListener(‘input’, (e) => {
input.value = isCurrency ? formatInputCurrency(e.target.value) : e.target.value;
calculate();
});
input.addEventListener(‘change’, (e) => {
let value = parseFloat(e.target.value.replace(/,/g, ”));
const min = parseFloat(slider.min);
const max = parseFloat(slider.max);
if (isNaN(value)) value = min;
if (value < min) value = min;
if (value > max) value = max;
slider.value = value;
input.value = isCurrency ? formatInputCurrency(value) : value;
calculate();
});
}
syncInputs(amountSlider, amountInput, true);
syncInputs(interestSlider, interestInput, false);
syncInputs(tenureSlider, tenureInput, false);
// Event listener for accordion functionality
scheduleBody.addEventListener(‘click’, function(e) {
const yearRow = e.target.closest(‘.year-summary-row’);
if (!yearRow) return;
const year = yearRow.dataset.year;
const detailsRow = document.querySelector(`.monthly-details[data-details-for-year=”${year}”]`);
const icon = yearRow.querySelector(‘.expand-icon’);
if (detailsRow) {
const isHidden = detailsRow.classList.contains(‘hidden’);
detailsRow.classList.toggle(‘hidden’);
icon.textContent = isHidden ? ‘−’ : ‘+’;
icon.style.transform = isHidden ? ‘rotate(45deg)’ : ‘rotate(0deg)’;
}
});
// Initial setup
function initialize() {
amountInput.value = formatInputCurrency(amountSlider.value);
interestInput.value = interestSlider.value;
tenureInput.value = tenureSlider.value;
calculate();
}
initialize();
});
</script>
</body>
</html>