Every real estate site I have built for a client ended up with the same request about two weeks after launch: "Can we show buyers what the monthly payment would be?" The first time, I wrote the calculator from scratch. The second time, I copied it and fixed the bugs I had shipped the first time. By the fourth site I packaged it as the Mortgage calculator in Facade. This article walks through how it works, so you can build your own or install mine and know what is inside.
The payment formula, explained once
A fixed rate mortgage payment is one formula. Everything else in the component is presentation.
With a loan amount P, a monthly interest rate r (the annual rate divided by 12 and by 100) and a number of monthly payments n (years times 12), the monthly principal and interest payment is:
M = P * r * (1 + r)^n / ((1 + r)^n - 1)
Two edge cases matter in code. When the rate is zero, the formula divides by zero, so you return P / n instead. When the down payment reaches 100 percent, the loan amount is zero and the payment is zero. Both happen the moment a visitor drags a slider to the end, so handle them before you touch any animation.
In TypeScript:
export function monthlyPayment(loan: number, annualRate: number, years: number) {
const n = years * 12;
if (loan <= 0) return 0;
if (annualRate === 0) return loan / n;
const r = annualRate / 100 / 12;
const f = Math.pow(1 + r, n);
return (loan * r * f) / (f - 1);
}
Principal and interest are only part of what a buyer pays each month. US buyers expect property tax, homeowners insurance and, for condos, HOA dues on the same screen. Tax is an annual percentage of the home price divided by 12. Insurance is an annual dollar amount divided by 12. HOA is already monthly. The total is the sum of the four, and that total is the number the visitor remembers.
Controlled inputs with sliders
The calculator has four inputs: home price, down payment, interest rate and loan term. The first three are range inputs with the formatted value printed next to the label, so a buyer always sees the exact number the slider is on. If you add a text input next to each slider, make both controls write to the same piece of state, which keeps them in sync without effects.
Down payment is the input that trips people up. Buyers think in percent, lenders think in dollars, and the slider has to show both. Store the percent, derive the dollar amount, and clamp the percent between 0 and 50 so the slider stays useful. A 90 percent down payment is technically valid and practically never happens.
A few details that make the inputs feel right:
- Format the displayed values with
Intl.NumberFormat, not with string concatenation.$1,250,000reads,$1250000does not. - Pick a sensible step for the price slider, a few thousand dollars at least. Pixel precision on a 2 million dollar range is useless.
- Label every range with a visible
<label>and anaria-valuetextthat reads the formatted value. Screen readers announce "650,000 dollars", not "650000".
The 15 or 30 year term toggle
Most calculators put the term in a select. A segmented toggle with two options is faster and shows both choices at once. In Facade it is a small two button group that moves a sliding pill behind the active option, and the payment re-renders the moment it changes. Buyers click it back and forth to feel the difference, which is exactly what you want them doing on a listing page.
Under the hood it is a role="radiogroup" with two role="radio" buttons, arrow keys to switch, and the defaultTerm prop set to 15 or 30. Nothing fancy, but the arrow keys matter: a toggle that only works with a mouse is a bug.
A donut breakdown with motion
A single total hides where the money goes. The donut shows four segments: principal and interest, tax, insurance and HOA. Each segment is an SVG circle with a strokeDasharray for its share and a strokeDashoffset to position it after the previous segment.
Animating the segments is where motion/react earns its place. Each circle's strokeDasharray and strokeDashoffset are animated when the values change, so dragging the price slider makes the segments breathe instead of jumping. The total in the middle of the donut uses the same count-up logic as the free Animated counter: it tweens from the previous value to the new one over a few hundred milliseconds, formatted as currency on every frame.
Two rules I follow for this kind of animation:
- Animate on change, not on mount only. The first render can animate in, but the interesting moment is the slider drag.
- Respect
prefers-reduced-motion.useReducedMotion()from motion returns true for those visitors, and the component then sets values directly instead of springing to them.
Install it with the shadcn CLI
The Mortgage calculator is a Pro item, so it installs from the private registry once your license key is in components.json (the installation docs show the one time setup).
npx shadcn@latest add @facade/mortgage-calculator
Then drop it on a listing page with the listing's price as the default and your market's tax rate:
import { MortgageCalculator, type MortgageBreakdown } from "@/components/facade/mortgage-calculator";
export function ListingPayment({ price }: { price: number }) {
return (
<MortgageCalculator
defaultPrice={price}
defaultDownPercent={20}
defaultRate={6.5}
defaultTerm={30}
propertyTaxRate={1.1}
annualInsurance={1800}
monthlyHoa={0}
title="Estimate your monthly payment"
onChange={(result: MortgageBreakdown) => {
// result.total, result.principalAndInterest, result.propertyTax, result.insurance, result.hoa, result.loanAmount
}}
/>
);
}
The onChange callback hands you the full breakdown on every change. I use it to update a "Payment from $X per month" line in the sticky header of the listing page, and some agents use it to prefill a pre-approval request.
If you only need the count-up number without the calculator, the Animated counter is free and MIT licensed:
npx shadcn@latest add https://facadeui.com/r/animated-counter.json
Where it belongs on a real estate site
On a listing page, the calculator goes directly under the key facts, before the photo gallery ends and the agent card appears. Buyers scroll to the price, then to the payment, then decide whether to keep reading. On a brokerage home page it earns less, because there is no price to anchor it to. The block order I use for agent sites is on the real estate agent page.
One thing I do not do: pull live rates from an API. Rates change daily, the calculator becomes a compliance question, and buyers understand that 6.5 percent is an example. Set a sensible default, let them change it, and put a one line disclaimer under the donut.
FAQ
Does the calculator include PMI?
No. Private mortgage insurance depends on the lender and the loan program, and a wrong PMI number is worse than none. If your market needs it, add it as a fifth monthly line in the onChange consumer or extend the component; the source is yours after installation.
Can I use it outside the US?
Yes. The formula is universal for fixed rate loans. Pass monthlyHoa={0}, set propertyTaxRate to your local rate or 0, and format currency with your own Intl locale in the consuming component. The labels are props, so "HOA" can become "Service charge" or disappear.
Why not use a mortgage calculator embed from a lender?
Embeds load a third party script, carry the lender's branding, and often break Core Web Vitals on mobile because they inject an iframe above the fold. A component you own renders as part of the page, inherits your theme tokens and sends leads to you, not to the lender.
Originally published at https://facadeui.com/blog/react-mortgage-calculator?utm_source=devto&utm_medium=social&utm_campaign=art-react-mortgage-calculator
Top comments (0)