Auto — Finance / Lease Calculator
A dealership finance desk calculator that estimates a vehicle payment live from price, down payment, trade-in, APR and term. Finance and lease tabs swap the underlying math: amortized APR for purchases, residual value and money factor for leases. Sliders paired with tabular money inputs recompute the monthly figure on every change, while a composition bar splits principal from interest and an amortization summary tallies amount financed, total of payments, cost of credit and cash due at signing.
MCP
Code
:root {
--garage: #141518;
--garage-2: #1f2127;
--steel: #5b6470;
--steel-l: #8a929d;
--orange: #ff6a13;
--orange-d: #e2540a;
--orange-50: #fff0e6;
--ink: #16181c;
--ink-2: #3b4049;
--muted: #737a85;
--bg: #f3f4f6;
--surface: #ffffff;
--line: rgba(20, 21, 24, 0.1);
--line-2: rgba(20, 21, 24, 0.18);
--ok: #2f9e6f;
--warn: #e0962a;
--danger: #d4493e;
--inprogress: #2b7fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--shadow: 0 1px 2px rgba(20, 21, 24, 0.06), 0 8px 24px rgba(20, 21, 24, 0.07);
--shadow-lg: 0 18px 50px rgba(20, 21, 24, 0.16);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 480px at 88% -8%, rgba(255, 106, 19, 0.08), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.mono { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.shell {
max-width: 1040px;
margin: 0 auto;
padding: 22px 20px 56px;
}
/* ---------- top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
background: linear-gradient(120deg, var(--garage), var(--garage-2));
color: #fff;
border-radius: var(--r-lg);
padding: 14px 18px;
box-shadow: var(--shadow);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 42px; height: 42px;
border-radius: var(--r-sm);
background: var(--orange);
color: #fff;
font-weight: 800;
letter-spacing: 0.5px;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12);
}
.brand-txt { display: flex; flex-direction: column; line-height: 1.25; }
.brand-txt strong { font-size: 16px; font-weight: 700; }
.brand-txt span { font-size: 12px; color: var(--steel-l); }
.vehicle-chip {
display: flex; align-items: center; gap: 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--r-md);
padding: 8px 14px 8px 8px;
}
.vc-photo {
width: 46px; height: 38px; border-radius: var(--r-sm);
background: linear-gradient(135deg, #2b7fff33, var(--steel) 70%, var(--garage));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.vc-txt { display: flex; flex-direction: column; line-height: 1.3; }
.vc-txt strong { font-size: 13.5px; }
.vc-txt span { font-size: 11px; color: var(--steel-l); }
.vc-txt b { color: #fff; font-weight: 600; }
/* ---------- layout ---------- */
.grid {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 18px;
margin-top: 18px;
align-items: start;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 18px;
}
/* ---------- tabs ---------- */
.tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px;
margin-bottom: 18px;
}
.tab {
appearance: none;
border: 0;
background: transparent;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--muted);
padding: 9px;
border-radius: 10px;
cursor: pointer;
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
}
.tab:hover { color: var(--ink-2); }
.tab.is-active {
background: var(--surface);
color: var(--ink);
box-shadow: var(--shadow), inset 0 0 0 1px var(--orange);
}
/* ---------- fields ---------- */
.field { margin-bottom: 16px; }
.field.two { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.field label {
display: flex; align-items: center; gap: 8px;
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
margin-bottom: 7px;
}
.pct-tag {
margin-left: auto;
font-size: 11.5px;
color: var(--orange-d);
background: var(--orange-50);
border-radius: 999px;
padding: 2px 8px;
font-weight: 700;
}
.money-input {
display: flex; align-items: center;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
overflow: hidden;
transition: box-shadow 0.16s, border-color 0.16s;
}
.money-input:focus-within {
border-color: var(--orange);
box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.16);
}
.money-input span {
padding: 0 10px;
color: var(--muted);
font-weight: 600;
background: var(--bg);
align-self: stretch;
display: flex; align-items: center;
border-right: 1px solid var(--line);
}
.money-input input {
border: 0; outline: 0;
font: inherit; font-weight: 600; font-size: 15px;
padding: 10px 12px;
width: 100%;
color: var(--ink);
background: transparent;
}
.range {
-webkit-appearance: none; appearance: none;
width: 100%;
height: 6px;
margin-top: 11px;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--line);
cursor: pointer;
}
.range::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--orange);
border: 3px solid #fff;
box-shadow: 0 1px 4px rgba(20, 21, 24, 0.3);
cursor: grab;
transition: transform 0.1s;
}
.range::-webkit-slider-thumb:active { transform: scale(1.12); cursor: grabbing; }
.range::-moz-range-thumb {
width: 20px; height: 20px;
border-radius: 50%;
background: var(--orange);
border: 3px solid #fff;
box-shadow: 0 1px 4px rgba(20, 21, 24, 0.3);
cursor: grab;
}
.range:focus-visible { outline: 2px solid var(--orange); outline-offset: 3px; }
.hint { font-size: 11.5px; color: var(--muted); margin: 8px 0 0; }
.quick {
display: flex; align-items: center; gap: 12px;
margin: 18px 0 4px;
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
appearance: none; border: 1px solid var(--line-2);
background: var(--surface);
font: inherit; font-weight: 600; font-size: 13px;
color: var(--ink-2);
width: 42px; padding: 7px 0;
border-radius: var(--r-sm);
cursor: pointer;
transition: all 0.15s;
}
.chip:hover { border-color: var(--steel); }
.chip.is-on {
background: var(--garage); color: #fff; border-color: var(--garage);
}
.reset {
appearance: none; border: 1px dashed var(--line-2);
background: transparent;
font: inherit; font-size: 12.5px; font-weight: 600;
color: var(--muted);
width: 100%;
padding: 10px;
margin-top: 14px;
border-radius: var(--r-sm);
cursor: pointer;
transition: all 0.15s;
}
.reset:hover { color: var(--orange-d); border-color: var(--orange); background: var(--orange-50); }
/* ---------- output ---------- */
.payout {
background: linear-gradient(135deg, var(--garage), var(--garage-2));
color: #fff;
border-radius: var(--r-md);
padding: 18px 20px;
position: relative;
overflow: hidden;
}
.payout::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(420px 200px at 90% -30%, rgba(255, 106, 19, 0.3), transparent 65%);
pointer-events: none;
}
.payout-head { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--steel-l); }
.status-dot {
width: 9px; height: 9px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 4px rgba(47, 158, 111, 0.22);
}
.status-dot.lease { background: var(--inprogress); box-shadow: 0 0 0 4px rgba(43, 127, 255, 0.22); }
.payout-amount {
display: flex; align-items: baseline; gap: 2px;
margin: 6px 0 2px;
}
.payout-amount .cur { font-size: 22px; font-weight: 700; color: var(--orange); }
.payout-amount #monthly { font-size: 46px; font-weight: 800; letter-spacing: -1.5px; line-height: 1; }
.payout-amount .per { font-size: 15px; color: var(--steel-l); font-weight: 600; }
.payout-sub { font-size: 12.5px; color: var(--steel-l); position: relative; }
.bar {
display: flex;
height: 14px;
border-radius: 999px;
overflow: hidden;
margin: 16px 0 10px;
background: var(--bg);
border: 1px solid var(--line);
}
.bar-seg { transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
.seg-principal { background: var(--orange); }
.seg-interest { background: var(--steel); }
.legend {
display: flex; flex-wrap: wrap; gap: 16px;
font-size: 12px; color: var(--ink-2);
margin-bottom: 16px;
}
.legend b { margin-left: 5px; color: var(--ink); }
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
.breakdown {
list-style: none; margin: 0; padding: 0;
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.breakdown li {
display: flex; justify-content: space-between; align-items: center;
padding: 11px 14px;
font-size: 13.5px;
border-bottom: 1px solid var(--line);
}
.breakdown li:last-child { border-bottom: 0; }
.breakdown li span { color: var(--ink-2); }
.breakdown li b { font-weight: 700; }
.breakdown li.grand {
background: var(--garage); color: #fff;
}
.breakdown li.grand span { color: var(--steel-l); }
.breakdown li.grand b { color: var(--orange); font-size: 15px; }
.status-panels {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
margin: 16px 0;
}
.sp {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 11px 13px;
display: flex; flex-direction: column; gap: 2px;
background: var(--bg);
transition: border-color 0.2s, background 0.2s;
}
.sp span { font-size: 11.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; }
.sp b { font-size: 15px; color: var(--ink); }
.sp.sp-on { border-color: rgba(47, 158, 111, 0.4); background: rgba(47, 158, 111, 0.07); }
.sp.sp-on b { color: var(--ok); }
.sp.sp-warn { border-color: rgba(224, 150, 42, 0.45); background: rgba(224, 150, 42, 0.08); }
.sp.sp-warn b { color: var(--warn); }
.cta {
appearance: none; border: 0;
width: 100%;
background: var(--orange);
color: #fff;
font: inherit; font-weight: 700; font-size: 15px;
padding: 13px;
border-radius: var(--r-md);
cursor: pointer;
box-shadow: 0 8px 22px rgba(255, 106, 19, 0.32);
transition: transform 0.12s, background 0.16s, box-shadow 0.16s;
}
.cta:hover { background: var(--orange-d); }
.cta:active { transform: translateY(1px); }
.cta:focus-visible { outline: 3px solid rgba(255, 106, 19, 0.4); outline-offset: 2px; }
.cta.locked { background: var(--ok); box-shadow: none; cursor: default; }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%; bottom: 24px;
transform: translate(-50%, 28px);
background: var(--garage);
color: #fff;
font-size: 13.5px; font-weight: 600;
padding: 12px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow-lg);
border-left: 3px solid var(--orange);
opacity: 0;
pointer-events: none;
transition: opacity 0.24s, transform 0.24s;
z-index: 40;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 820px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.shell { padding: 16px 13px 48px; }
.topbar { padding: 12px 14px; }
.vehicle-chip { width: 100%; }
.field.two { grid-template-columns: 1fr; gap: 16px; }
.payout-amount #monthly { font-size: 38px; }
.quick { flex-direction: column; align-items: flex-start; gap: 8px; }
.chip { flex: 1; min-width: 42px; }
}(function () {
"use strict";
var DEFAULTS = { price: 42850, down: 9000, trade: 6500, apr: 6.4, term: 72, residual: 58 };
var TAX_RATE = 0.0825; // sales tax on the financed/cap-cost basis
var LEASE_FEES = 895; // acquisition + doc fees rolled into a lease
var mode = "finance";
var locked = false;
var $ = function (id) { return document.getElementById(id); };
var els = {
price: $("price"), priceR: $("price-r"),
down: $("down"), downR: $("down-r"), downPct: $("down-pct"),
trade: $("trade"), tradeR: $("trade-r"),
aprR: $("apr-r"), aprTag: $("apr-tag"),
termR: $("term-r"), termTag: $("term-tag"),
residualR: $("residual-r"), residualPct: $("residual-pct"),
monthly: $("monthly"), payoutSub: $("payout-sub"),
dealLabel: $("deal-label"), dealDot: $("deal-dot"),
segP: $("seg-principal"), segI: $("seg-interest"),
lgPrincipal: $("lg-principal"), lgInterest: $("lg-interest"), lgRent: $("lg-rent"),
bFinanced: $("b-financed"), bTotal: $("b-total"), bInterest: $("b-interest"),
bSigning: $("b-signing"), bCredit: $("b-credit"),
spAff: $("sp-affordable"), spAffVal: $("sp-aff-val"),
apply: $("apply"), reset: $("reset"),
toast: $("toast")
};
// ---------- helpers ----------
function money(n) {
n = Math.max(0, Math.round(n));
return "$" + n.toLocaleString("en-US");
}
function money2(n) {
return "$" + (Math.round(n * 100) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function num(el) { var v = parseFloat(el.value); return isFinite(v) ? v : 0; }
var toastTimer;
function toast(msg) {
els.toast.textContent = msg;
els.toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { els.toast.classList.remove("show"); }, 2600);
}
// ---------- core math ----------
function compute() {
var price = num(els.price);
var down = Math.min(num(els.down), price);
var trade = num(els.trade);
var apr = num(els.aprR);
var term = num(els.termR);
var residualPct = num(els.residualR);
var out;
if (mode === "finance") {
// Sales tax applied to price minus trade-in credit.
var taxable = Math.max(0, price - trade);
var tax = taxable * TAX_RATE;
var financed = Math.max(0, price + tax - down - trade);
var r = apr / 100 / 12;
var monthly = term > 0
? (r > 0 ? financed * r / (1 - Math.pow(1 + r, -term)) : financed / term)
: 0;
var totalPayments = monthly * term;
var interest = Math.max(0, totalPayments - financed);
out = {
monthly: monthly,
financed: financed,
total: totalPayments,
interest: interest,
signing: down,
credit: interest,
principalShare: totalPayments > 0 ? financed / totalPayments : 1,
rentLabel: false
};
} else {
// Lease: payment = depreciation + rent charge, on a capitalized cost.
var residual = price * (residualPct / 100);
var capCost = Math.max(residual, price - down - trade + LEASE_FEES);
var depreciation = (capCost - residual) / term;
var mf = (apr / 100) / 24; // money factor from APR
var rent = (capCost + residual) * mf;
var monthlyL = depreciation + rent;
var totalL = monthlyL * term + down;
var rentTotal = rent * term + LEASE_FEES;
out = {
monthly: monthlyL,
financed: capCost,
total: totalL,
interest: rentTotal,
signing: down + monthlyL, // first payment + down at signing
credit: rentTotal,
principalShare: monthlyL > 0 ? depreciation / monthlyL : 0.7,
rentLabel: true,
residual: residual
};
}
return { o: out, price: price, down: down, apr: apr, term: term, residualPct: residualPct };
}
function render() {
var c = compute();
var o = c.o;
// headline
els.monthly.textContent = Math.round(o.monthly).toLocaleString("en-US");
els.payoutSub.textContent = mode === "finance"
? "over " + c.term + " months · " + c.apr.toFixed(2) + "% APR"
: "over " + c.term + " months · " + c.residualPct + "% residual";
// composition bar
var pShare = Math.max(0.04, Math.min(0.96, o.principalShare));
els.segP.style.width = (pShare * 100).toFixed(1) + "%";
els.segI.style.width = ((1 - pShare) * 100).toFixed(1) + "%";
els.lgPrincipal.textContent = money(o.total - o.interest);
if (o.rentLabel) {
els.lgRent.textContent = money(o.interest);
} else {
els.lgInterest.textContent = money(o.interest);
}
// breakdown
els.bFinanced.textContent = money(o.financed);
els.bTotal.textContent = money(o.total);
els.bInterest.textContent = money(o.interest);
els.bSigning.textContent = money(o.signing);
els.bCredit.textContent = money(o.credit);
// tags
var dp = c.price > 0 ? Math.round((c.down / c.price) * 100) : 0;
els.downPct.textContent = dp + "%";
els.aprTag.textContent = c.apr.toFixed(2) + "%";
els.termTag.textContent = c.term + " mo";
els.residualPct.textContent = c.residualPct + "%";
// affordability gauge (rule of thumb: payment vs a $5,600/mo budget band)
var ratio = o.monthly / 5600;
var aff = els.spAff;
aff.classList.remove("sp-on", "sp-warn");
if (ratio <= 0.13) { aff.classList.add("sp-on"); els.spAffVal.textContent = "Comfortable"; }
else if (ratio <= 0.2) { aff.classList.add("sp-warn"); els.spAffVal.textContent = "Stretch"; }
else { aff.classList.add("sp-warn"); els.spAffVal.textContent = "Over budget"; }
}
// ---------- sync input <-> range pairs ----------
function pair(input, range) {
input.addEventListener("input", function () {
range.value = input.value;
render();
});
range.addEventListener("input", function () {
input.value = range.value;
render();
});
}
pair(els.price, els.priceR);
pair(els.down, els.downR);
pair(els.trade, els.tradeR);
els.aprR.addEventListener("input", render);
els.residualR.addEventListener("input", render);
els.termR.addEventListener("input", function () {
syncTermChips(num(els.termR));
render();
});
// ---------- term chips ----------
var chips = Array.prototype.slice.call(document.querySelectorAll("#term-chips .chip"));
function syncTermChips(term) {
chips.forEach(function (ch) {
ch.classList.toggle("is-on", parseInt(ch.dataset.term, 10) === term);
});
}
chips.forEach(function (ch) {
ch.addEventListener("click", function () {
var t = parseInt(ch.dataset.term, 10);
els.termR.value = t;
syncTermChips(t);
render();
});
});
// ---------- finance / lease tabs ----------
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
function setMode(next) {
mode = next;
tabs.forEach(function (t) {
var on = t.dataset.mode === next;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
});
var isLease = next === "lease";
document.querySelectorAll(".lbl-finance").forEach(function (e) { e.hidden = isLease; });
document.querySelectorAll(".lbl-lease").forEach(function (e) { e.hidden = !isLease; });
document.querySelectorAll(".lease-only").forEach(function (e) { e.hidden = !isLease; });
els.dealLabel.textContent = isLease
? "Estimated monthly lease payment"
: "Estimated monthly finance payment";
els.dealDot.classList.toggle("lease", isLease);
render();
toast(isLease ? "Switched to lease — residual & money factor applied" : "Switched to finance — APR amortization");
}
tabs.forEach(function (t) {
t.addEventListener("click", function () { setMode(t.dataset.mode); });
});
// ---------- reset ----------
els.reset.addEventListener("click", function () {
els.price.value = els.priceR.value = DEFAULTS.price;
els.down.value = els.downR.value = DEFAULTS.down;
els.trade.value = els.tradeR.value = DEFAULTS.trade;
els.aprR.value = DEFAULTS.apr;
els.termR.value = DEFAULTS.term;
els.residualR.value = DEFAULTS.residual;
syncTermChips(DEFAULTS.term);
render();
toast("Reset to MSRP defaults");
});
// ---------- lock quote ----------
els.apply.addEventListener("click", function () {
if (locked) return;
locked = true;
els.apply.classList.add("locked");
var c = compute();
els.apply.textContent = "Quote locked · " + money2(c.o.monthly) + "/mo";
toast("Quote locked for 72 hours — ref #RD-" + (Math.floor(Math.random() * 9000) + 1000));
setTimeout(function () {
locked = false;
els.apply.classList.remove("locked");
els.apply.textContent = "Lock this quote";
}, 4200);
});
// initial paint
syncTermChips(DEFAULTS.term);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Finance / Lease Calculator</title>
<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;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell" role="main">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">RD</span>
<div class="brand-txt">
<strong>Rivetline Motors</strong>
<span>Finance & Lease Desk</span>
</div>
</div>
<div class="vehicle-chip">
<span class="vc-photo" aria-hidden="true"></span>
<div class="vc-txt">
<strong>2024 Bishop Trail XLT</strong>
<span>VIN <b class="mono">1RVB24XLT0091847</b> · Stock <b class="mono">#D-2241</b></span>
</div>
</div>
</header>
<div class="grid">
<!-- LEFT: inputs -->
<section class="panel inputs" aria-label="Deal inputs">
<div class="tabs" role="tablist" aria-label="Financing type">
<button class="tab is-active" role="tab" aria-selected="true" data-mode="finance" id="tab-finance">Finance</button>
<button class="tab" role="tab" aria-selected="false" data-mode="lease" id="tab-lease">Lease</button>
</div>
<div class="field">
<label for="price">Vehicle price (MSRP)</label>
<div class="money-input">
<span>$</span>
<input id="price" class="mono" type="number" inputmode="numeric" min="0" step="100" value="42850" />
</div>
<input id="price-r" class="range" type="range" min="8000" max="120000" step="250" value="42850" aria-label="Vehicle price slider" />
</div>
<div class="field">
<label for="down">Down payment <span class="pct-tag mono" id="down-pct">21%</span></label>
<div class="money-input">
<span>$</span>
<input id="down" class="mono" type="number" inputmode="numeric" min="0" step="100" value="9000" />
</div>
<input id="down-r" class="range" type="range" min="0" max="60000" step="100" value="9000" aria-label="Down payment slider" />
</div>
<div class="field">
<label for="trade">Trade-in value</label>
<div class="money-input">
<span>$</span>
<input id="trade" class="mono" type="number" inputmode="numeric" min="0" step="100" value="6500" />
</div>
<input id="trade-r" class="range" type="range" min="0" max="60000" step="100" value="6500" aria-label="Trade-in slider" />
</div>
<div class="field two">
<div>
<label for="apr"><span class="lbl-finance">APR</span><span class="lbl-lease" hidden>Money factor (×2400)</span> <span class="pct-tag mono" id="apr-tag">6.40%</span></label>
<input id="apr-r" class="range" type="range" min="0" max="18" step="0.05" value="6.4" aria-label="APR slider" />
</div>
<div>
<label for="term">Term <span class="pct-tag mono" id="term-tag">72 mo</span></label>
<input id="term-r" class="range" type="range" min="12" max="84" step="12" value="72" aria-label="Term slider" />
</div>
</div>
<div class="field lease-only" hidden>
<label for="residual">Residual value <span class="pct-tag mono" id="residual-pct">58%</span></label>
<input id="residual-r" class="range" type="range" min="30" max="75" step="1" value="58" aria-label="Residual percent slider" />
<p class="hint">Estimated lease-end value of the vehicle.</p>
</div>
<div class="quick">
<span>Quick term</span>
<div class="chips" id="term-chips">
<button class="chip" data-term="36">36</button>
<button class="chip" data-term="48">48</button>
<button class="chip" data-term="60">60</button>
<button class="chip is-on" data-term="72">72</button>
<button class="chip" data-term="84">84</button>
</div>
</div>
<button class="reset" id="reset" type="button">Reset to MSRP defaults</button>
</section>
<!-- RIGHT: output -->
<section class="panel output" aria-label="Payment estimate">
<div class="payout">
<div class="payout-head">
<span class="status-dot" id="deal-dot"></span>
<span id="deal-label">Estimated monthly finance payment</span>
</div>
<div class="payout-amount">
<span class="cur">$</span>
<span class="mono" id="monthly">0</span>
<span class="per">/mo</span>
</div>
<div class="payout-sub" id="payout-sub">over 72 months · 6.40% APR</div>
</div>
<div class="bar" aria-label="Payment composition" id="composition">
<div class="bar-seg seg-principal" id="seg-principal" style="width:70%"></div>
<div class="bar-seg seg-interest" id="seg-interest" style="width:30%"></div>
</div>
<div class="legend">
<span><i class="sw seg-principal"></i> Principal <b class="mono" id="lg-principal">$0</b></span>
<span class="lbl-finance"><i class="sw seg-interest"></i> Interest <b class="mono" id="lg-interest">$0</b></span>
<span class="lbl-lease" hidden><i class="sw seg-interest"></i> Rent + fees <b class="mono" id="lg-rent">$0</b></span>
</div>
<ul class="breakdown" aria-label="Amortization summary">
<li><span>Amount financed</span><b class="mono" id="b-financed">$0</b></li>
<li><span class="lbl-finance">Total of payments</span><span class="lbl-lease" hidden>Total lease cost</span><b class="mono" id="b-total">$0</b></li>
<li><span class="lbl-finance">Total interest</span><span class="lbl-lease" hidden>Total rent charge</span><b class="mono" id="b-interest">$0</b></li>
<li><span>Due at signing</span><b class="mono" id="b-signing">$0</b></li>
<li class="grand"><span>Cost of credit</span><b class="mono" id="b-credit">$0</b></li>
</ul>
<div class="status-panels" aria-label="Deal status">
<div class="sp sp-on" id="sp-approved"><span>Pre-qualified</span><b class="mono">A+ tier</b></div>
<div class="sp" id="sp-affordable"><span>Affordability</span><b class="mono" id="sp-aff-val">—</b></div>
</div>
<button class="cta" id="apply" type="button">Lock this quote</button>
</section>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Finance / Lease Calculator
A single-screen finance desk for Rivetline Motors, built around a 2024 Bishop Trail XLT with its VIN and stock number on a gradient vehicle chip. The left panel collects the deal inputs — vehicle price, down payment, trade-in value, APR and term — each one a tabular money field wired to a slider so a value can be typed or dragged. Quick-term chips jump the loan to 36, 48, 60, 72 or 84 months, and a down-payment percentage tag updates as the numbers move.
The Finance and Lease tabs swap the math behind the headline. Finance amortizes the taxed, trade-adjusted balance over the term at the chosen APR; Lease reveals a residual-value slider and recalculates payment as depreciation plus a money-factor rent charge on the capitalized cost. Every keystroke or drag recomputes the large monthly figure, repaints the principal-versus-interest composition bar, and refreshes the amortization summary — amount financed, total of payments, total interest or rent, cash due at signing and cost of credit. An affordability panel flags the payment as comfortable, a stretch or over budget, and locking the quote stamps a reference number via a toast.
Everything is vanilla HTML, CSS and JavaScript with no frameworks, build step or libraries beyond the Inter font. Money and rate fields use tabular figures, the two-column layout collapses to a stacked mobile view below 520px, and sliders, inputs, chips and buttons all stay keyboard-usable.
Illustrative UI only — fictional shop/dealership, not a real service system.