Debt-to-Income (DTI) Ratio Calculator

Compute your back-end and front-end DTI, classify risk, and estimate the maximum affordable new payment against a chosen DTI limit.

Income + Debt Inputs

Use pre-tax income for underwriting parity.

Include rent or mortgage (PITI/HOA if applicable).

How to Use This Calculator

Enter your gross annual income, choose a DTI cap that aligns with your lender or financial plan, and enter all recurring monthly debts. Include housing, credit cards, auto, and other installment payments. Tap Calculate to refresh the metrics and see whether your current obligations fit the cap.

Methodology

The calculator converts annual income into gross monthly income and compares it to recurring obligations. Back-end DTI captures total debts, while front-end isolates the housing payment. The max additional payment shows how much more you could afford without breaching the chosen cap.

Results aim to mirror typical underwriting practice. Percentages display one decimal place, and capacity assumes any remaining room for additional debt given the selected cap. Actual approvals will depend on lender criteria.

Guidelines

  • < 36% generally strong.
  • 36–43% fair to caution, depending on program.
  • > 43% high risk; underwriting exceptions may be needed.
Formulas

Back-end DTI = total monthly debts ÷ gross monthly income.

Front-end DTI = housing payment ÷ gross monthly income.

Max additional payment @ cap = (cap% × gross monthly income) − total monthly debts (and never negative).

Original audit note

This section shows the formulas used by the calculator engine, plus variable definitions and units.

\[','\]

Variables:

  • T = property tax (annual or monthly depending on input) (currency)
  • HOA = homeowners association dues (monthly) (currency)
Citations

Authoritative sources referenced by the original audit:

Changelog
  • 0.1.0-draft — 2026-01-19: Initial audit spec draft generated from HTML extraction (review required).
Verified by Ugo Candido on 2026-01-19 Audit: Complete Version 0.1.0-draft Last Updated: 2026-01-19
Version 1.5.0
+ Math.round(value).toString(); } }; const classify = (ratio) => { if (!Number.isFinite(ratio)) return '—'; if (ratio < 0.36) return 'Strong'; if (ratio <= 0.43) return 'Fair / Caution'; return 'High Risk'; }; const defaults = { income: 75000, cap: 36, housing: 1500, credit: 150, auto: 350, other: 200 }; const els = { income: document.getElementById('dti_income'), cap: document.getElementById('dti_cap'), housing: document.getElementById('dti_housing'), credit: document.getElementById('dti_cc'), auto: document.getElementById('dti_auto'), other: document.getElementById('dti_student'), back: document.getElementById('dti_back'), front: document.getElementById('dti_front'), riskClass: document.getElementById('dti_class'), capacity: document.getElementById('dti_capacity'), meterBack: document.getElementById('meter_back'), meterFront: document.getElementById('meter_front'), errorBox: document.getElementById('errorBox'), calcBtn: document.getElementById('calcBtn'), resetBtn: document.getElementById('resetBtn') }; function parseInputs() { return { income: parseFloat(els.income.value), cap: parseFloat(els.cap.value), housing: parseFloat(els.housing.value), credit: parseFloat(els.credit.value), auto: parseFloat(els.auto.value), other: parseFloat(els.other.value) }; } function validate(inputs) { const errors = []; if (!Number.isFinite(inputs.income) || inputs.income <= 0) { errors.push('Gross income must be greater than 0.'); } if (!Number.isFinite(inputs.cap) || inputs.cap < 0) { errors.push('DTI cap must be 0 or greater.'); } ['housing', 'credit', 'auto', 'other'].forEach((field) => { if (!Number.isFinite(inputs[field]) || inputs[field] < 0) { errors.push('Monthly debts must be 0 or greater.'); } }); return { ok: errors.length === 0, errors }; } function compute(inputs) { const grossMonthly = inputs.income / 12; const totalDebts = inputs.housing + inputs.credit + inputs.auto + inputs.other; const back = grossMonthly > 0 ? totalDebts / grossMonthly : NaN; const front = grossMonthly > 0 ? inputs.housing / grossMonthly : NaN; const capDecimal = inputs.cap / 100; const capacity = Number.isFinite(capDecimal) && grossMonthly > 0 ? Math.max(0, capDecimal * grossMonthly - totalDebts) : NaN; return { back, front, capacity, classification: classify(back), grossMonthly, totalDebts }; } function format(outputs) { const safeClamp = (value) => { if (!Number.isFinite(value)) return 0; return Math.min(Math.max(value, 0), 1) * 100; }; return { back: fmtPercent(outputs.back), front: fmtPercent(outputs.front), classification: outputs.classification, capacity: fmtCurrency(outputs.capacity), meterBack: safeClamp(outputs.back), meterFront: safeClamp(outputs.front) }; } function render(formatted) { els.back.textContent = formatted.back; els.front.textContent = formatted.front; els.riskClass.innerHTML = `${formatted.classification}`; els.capacity.textContent = formatted.capacity; els.meterBack.style.width = `${formatted.meterBack}%`; els.meterFront.style.width = `${formatted.meterFront}%`; } function renderError(errors) { if (!errors || errors.length === 0) { els.errorBox.style.display = 'none'; els.errorBox.textContent = ''; return; } els.errorBox.style.display = 'block'; els.errorBox.textContent = errors.join(' '); } function update() { const inputs = parseInputs(); const validation = validate(inputs); if (!validation.ok) { renderError(validation.errors); return; } renderError([]); const outputs = compute(inputs); const formatted = format(outputs); render(formatted); } function resetInputs() { els.income.value = defaults.income; els.cap.value = defaults.cap; els.housing.value = defaults.housing; els.credit.value = defaults.credit; els.auto.value = defaults.auto; els.other.value = defaults.other; } ['income', 'cap', 'housing', 'credit', 'auto', 'other'].forEach((key) => { const node = els[key]; if (node) { node.addEventListener('input', debouncedUpdate); node.addEventListener('change', debouncedUpdate); } }); els.calcBtn.addEventListener('click', update); els.resetBtn.addEventListener('click', () => { resetInputs(); const debouncedUpdate = debounce(update, 100); document.querySelectorAll('#inputsCard input, #inputsCard select, #inputsCard textarea') .forEach((el) => { el.addEventListener('input', debouncedUpdate); el.addEventListener('change', debouncedUpdate); }); update(); }); resetInputs(); update(); })();