Where to Live in Phnom Penh: Expat Neighborhood & Sangkat Guide (2026)
For first-time expats, BKK1 offers unmatched walkability and international amenities, while Toul Tompoung (Russian Market) provides the best balance of community, trendy cafes, and affordable rent ($350â$600/month). Families often choose Toul Kork for spacious gated compounds and international schools.
MoveKH Rent & Budget Calculator
2026 Edition
30% Rule Baseline
Adjusts unit size & utilities
Avg 1-2 Bed Modern Units
Diff vs Phnom Penh
Phnom Penh
$1,170 / mo
Bangkok
+$490 / mo
Ho Chi Minh City
+$210 / mo
$5,880 / yr
47% of income
// 2026 Market Baseline Data
const DATA_2026 = {
rent: {
outer: 280, // Toul Kork, Sen Sok
hub: 360, // Russian Market (TTP), Daun Penh
central: 520, // BKK1, Tonle Bassac
luxury: 950, // High-rise Serviced / BKK1 Penthouse
bkk: 780, // Bangkok Sukhumvit/Sathorn 1-bed baseline
hcmc: 620 // HCMC District 1/2/Binh Thanh 1-bed baseline
},
living: {
pp: 650, // PP Non-rent: utilities, food, transport, leisure
bkk: 880, // Bangkok living baseline
hcmc: 760 // HCMC living baseline
}
};
// State
let state = {
salary: 2500,
household: 1,
district: 'central'
};
// Scoped Element Cache
const salaryInput = root.querySelector('#mk-in-salary');
const salaryRange = root.querySelector('#mk-in-range');
const householdButtons = root.querySelectorAll('.mk-pill-btn');
const districtButtons = root.querySelectorAll('.mk-district-card');
const maxRentDisplay = root.querySelector('#mk-val-max-rent');
const budgetStatus = root.querySelector('#mk-val-budget-status');
const targetRentDisplay = root.querySelector('#mk-val-target-rent');
const rentPctDisplay = root.querySelector('#mk-val-rent-pct');
const ppTotalDisplay = root.querySelector('#mk-val-pp-total');
const compPP = root.querySelector('#mk-comp-pp');
const compBKK = root.querySelector('#mk-comp-bkk');
const compHCMC = root.querySelector('#mk-comp-hcmc');
const annualSavings = root.querySelector('#mk-val-annual-savings');
const totalPctDisplay = root.querySelector('#mk-val-total-pct');
const barRent = root.querySelector('#mk-bar-rent');
const barLiving = root.querySelector('#mk-bar-living');
const legendRentPct = root.querySelector('#mk-legend-rent-pct');
const legendLivingPct = root.querySelector('#mk-legend-living-pct');
const legendFreePct = root.querySelector('#mk-legend-free-pct');
function formatCurrency(amount) {
return '$' + Math.round(amount).toLocaleString('en-US');
}
function updateSliderFill(val, min, max) {
if (!salaryRange) return;
const clampedVal = Math.min(Math.max(val, min), max);
const percentage = ((clampedVal - min) / (max - min)) * 100;
salaryRange.style.setProperty('--slider-fill', `${percentage}%`);
}
function recalculate() {
const salary = Math.max(0, state.salary);
const people = state.household;
const district = state.district;
// Multipliers for couples (larger unit + 1.6x living costs)
const livingMult = people === 2 ? 1.6 : 1.0;
const rentCoupleAdd = people === 2 ? 180 : 0;
const bkkCoupleAdd = people === 2 ? 220 : 0;
const hcmcCoupleAdd = people === 2 ? 200 : 0;
// Rent calculations
const actualRent = (DATA_2026.rent[district] || 520) + rentCoupleAdd;
const ppLiving = DATA_2026.living.pp * livingMult;
const ppTotal = actualRent + ppLiving;
// Comparative SEA costs
const bkkTotal = (DATA_2026.rent.bkk + bkkCoupleAdd) + (DATA_2026.living.bkk * livingMult);
const hcmcTotal = (DATA_2026.rent.hcmc + hcmcCoupleAdd) + (DATA_2026.living.hcmc * livingMult);
// Safe 30% Golden Rule
const maxRent = Math.floor(salary * 0.3);
// DOM Updates
if (maxRentDisplay) maxRentDisplay.textContent = formatCurrency(maxRent);
if (targetRentDisplay) targetRentDisplay.textContent = formatCurrency(actualRent);
if (ppTotalDisplay) ppTotalDisplay.textContent = formatCurrency(ppTotal);
// Status Badge logic
if (budgetStatus) {
budgetStatus.className = 'mk-budget-badge';
if (salary === 0) {
budgetStatus.classList.add('caution');
budgetStatus.innerHTML = 'âšī¸ Enter Monthly Salary';
} else if (actualRent <= maxRent) {
budgetStatus.classList.add('safe');
budgetStatus.innerHTML = 'â Within Safe 30% Budget';
} else if (actualRent <= salary * 0.4) {
budgetStatus.classList.add('caution');
budgetStatus.innerHTML = 'â ī¸ Moderate Rent Stretch (30-40%)';
} else {
budgetStatus.classList.add('warning');
budgetStatus.innerHTML = 'đ¨ High Rent Burden (>40%)';
}
}
// Percentages
const rentPct = salary > 0 ? Math.round((actualRent / salary) * 100) : 0;
const livingPct = salary > 0 ? Math.round((ppLiving / salary) * 100) : 0;
const totalPct = salary > 0 ? Math.min(100, rentPct + livingPct) : 0;
const freePct = Math.max(0, 100 - (rentPct + livingPct));
if (rentPctDisplay) {
rentPctDisplay.textContent = salary > 0 ? `${rentPct}% of monthly income` : 'Enter salary';
}
if (totalPctDisplay) {
totalPctDisplay.textContent = salary > 0 ? `${totalPct}% of monthly income` : '--';
}
// Progress bars
if (barRent) barRent.style.width = `${Math.min(100, rentPct)}%`;
if (barLiving) barLiving.style.width = `${Math.min(100 - Math.min(100, rentPct), livingPct)}%`;
if (legendRentPct) legendRentPct.textContent = `${rentPct}%`;
if (legendLivingPct) legendLivingPct.textContent = `${livingPct}%`;
if (legendFreePct) legendFreePct.textContent = `${freePct}%`;
// Comparisons
const bkkDiff = bkkTotal - ppTotal;
const hcmcDiff = hcmcTotal - ppTotal;
if (compPP) compPP.textContent = formatCurrency(ppTotal) + ' / mo';
if (compBKK) {
if (bkkDiff >= 0) {
compBKK.className = 'mk-diff-tag higher';
compBKK.textContent = `+${formatCurrency(bkkDiff)} / mo`;
} else {
compBKK.className = 'mk-diff-tag pp-base';
compBKK.textContent = `-${formatCurrency(Math.abs(bkkDiff))} / mo`;
}
}
if (compHCMC) {
if (hcmcDiff >= 0) {
compHCMC.className = 'mk-diff-tag higher';
compHCMC.textContent = `+${formatCurrency(hcmcDiff)} / mo`;
} else {
compHCMC.className = 'mk-diff-tag pp-base';
compHCMC.textContent = `-${formatCurrency(Math.abs(hcmcDiff))} / mo`;
}
}
// Annual Savings vs BKK
if (annualSavings) {
const annual = Math.max(0, bkkDiff * 12);
annualSavings.textContent = formatCurrency(annual) + ' / yr';
}
}
// Event Handlers
if (salaryInput) {
salaryInput.addEventListener('input', function() {
const val = parseFloat(salaryInput.value) || 0;
state.salary = val;
if (salaryRange) salaryRange.value = Math.min(Math.max(val, 500), 8000);
updateSliderFill(val, 500, 8000);
recalculate();
});
}
if (salaryRange) {
salaryRange.addEventListener('input', function() {
const val = parseFloat(salaryRange.value) || 500;
state.salary = val;
if (salaryInput) salaryInput.value = val;
updateSliderFill(val, 500, 8000);
recalculate();
});
updateSliderFill(state.salary, 500, 8000);
}
// Household Segmented Pills
householdButtons.forEach(btn => {
btn.addEventListener('click', function() {
householdButtons.forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-checked', 'false');
});
btn.classList.add('active');
btn.setAttribute('aria-checked', 'true');
state.household = parseInt(btn.dataset.household, 10) || 1;
recalculate();
});
});
// District Cards
districtButtons.forEach(card => {
card.addEventListener('click', function() {
districtButtons.forEach(c => {
c.classList.remove('active');
c.setAttribute('aria-checked', 'false');
});
card.classList.add('active');
card.setAttribute('aria-checked', 'true');
state.district = card.dataset.district || 'central';
recalculate();
});
});
root.dataset.initialized = 'true';
recalculate();
}
// Safe mounting across DOM states
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMoveKHCalculator);
} else {
initMoveKHCalculator();
}
// Fallback observer for dynamic block rendering
if (window.MutationObserver) {
const observer = new MutationObserver(function(mutations, obs) {
const el = document.getElementById('mk-rent-calc-2026');
if (el && el.dataset.initialized !== 'true') {
initMoveKHCalculator();
obs.disconnect();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 6000);
}
})();
Was this guide helpful?
Your feedback helps us keep MoveKH accurate.