Build an Investment Calculator 📈
"If I save $500 a month for 30 years at 7%, how much will I have?"
This is the classic Compound Interest problem.
It's not just Amount * Rate. You have to account for regular contributions.
In this guide, we will build a calculator that solves for the Future Value (FV) of an investment.
Step 1: The Math (Future Value Formula) 🧮
We need to calculate two things separately and add them up:
- Compound Interest on Principal: The initial money growing.
P * (1 + r)^t
- Future Value of Series: The monthly contributions growing.
PMT * [ ((1 + r)^t - 1) / r ]
Where:
P= Principal (Starting Amount)PMT= Monthly Contributionr= Monthly Interest Rate (Annual Rate / 12)t= Total Months (Years * 12)
Step 2: The Logic Implementation 🧠
const calculateFV = (principal, monthly, annualRate, years) => {
const r = (annualRate / 100) / 12; // Monthly Rate
const t = years * 12; // Total Months
// 1. Principal Growth
const spreadFV = principal * Math.pow(1 + r, t);
// 2. Contributions Growth
// If rate is 0, it's just linear addition
if (r === 0) return principal + (monthly * t);
const contributionFV = monthly * ((Math.pow(1 + r, t) - 1) / r);
return spreadFV + contributionFV;
}
Step 3: The Full React Component 💻
Here is a clean, responsive calculator for your users.
"use client"
import { useState } from "react"
import { DollarSign, TrendingUp, Calendar, Percent } from "lucide-react"
export default function InvestmentCalc() {
const [start, setStart] = useState(10000)
const [monthly, setMonthly] = useState(500)
const [rate, setRate] = useState(7)
const [years, setYears] = useState(30)
const calculateResult = () => {
const r = (rate / 100) / 12
const n = years * 12
// Principal Growth
const futurePrincipal = start * Math.pow(1 + r, n)
// Series Growth
const futureSeries = monthly * ((Math.pow(1 + r, n) - 1) / r)
return futurePrincipal + futureSeries
}
const result = calculateResult()
const totalInvested = start + (monthly * years * 12)
const totalInterest = result - totalInvested
return (
<div className="max-w-4xl mx-auto p-6 grid md:grid-cols-2 gap-8">
{/* INPUT SECTION */}
<div className="space-y-6 bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<h2 className="text-xl font-bold text-slate-800 mb-4 flex items-center gap-2">
<TrendingUp className="text-blue-600" /> Investment Plan
</h2>
<div>
<label className="text-xs font-bold text-slate-500 uppercase">Starting Amount</label>
<div className="relative mt-1">
<DollarSign className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="number" value={start} onChange={e => setStart(Number(e.target.value))}
className="w-full pl-9 p-2.5 bg-slate-50 border border-slate-200 rounded-lg font-medium"
/>
</div>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase">Monthly Contribution</label>
<div className="relative mt-1">
<DollarSign className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="number" value={monthly} onChange={e => setMonthly(Number(e.target.value))}
className="w-full pl-9 p-2.5 bg-slate-50 border border-slate-200 rounded-lg font-medium"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-500 uppercase">Annual Rate (%)</label>
<div className="relative mt-1">
<Percent className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="number" value={rate} onChange={e => setRate(Number(e.target.value))}
className="w-full pl-9 p-2.5 bg-slate-50 border border-slate-200 rounded-lg font-medium"
/>
</div>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase">Year to Grow</label>
<div className="relative mt-1">
<Calendar className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="number" value={years} onChange={e => setYears(Number(e.target.value))}
className="w-full pl-9 p-2.5 bg-slate-50 border border-slate-200 rounded-lg font-medium"
/>
</div>
</div>
</div>
</div>
{/* RESULT SECTION */}
<div className="bg-slate-900 text-white p-8 rounded-2xl shadow-xl flex flex-col justify-center space-y-8">
<div>
<div className="text-slate-400 font-medium mb-1">Estimated Total</div>
<div className="text-5xl font-bold tracking-tight text-blue-400">
${result.toLocaleString(undefined, {maximumFractionDigits: 0})}
</div>
</div>
<div className="space-y-4 pt-8 border-t border-slate-800">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400">Principal Invested</span>
<span className="font-mono font-bold">${totalInvested.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-400">Total Interest Earned</span>
<span className="font-mono font-bold text-green-400">+${totalInterest.toLocaleString(undefined, {maximumFractionDigits: 0})}</span>
</div>
</div>
<div className="text-xs text-slate-500 text-center leading-relaxed">
*Returns are compounded monthly. Inflation is not adjusted.
</div>
</div>
</div>
)
}
Step 4: Visualizing the Growth (Optional) 📊
A simple text result is good, but users love charts.
Because we calculate the balance year-by-year, you can easily use a library like Recharts to show the exponential curve.
Logic for Chart Data:
const data = [];
for(let i = 1; i <= years; i++) {
data.push({
year: i,
amount: calculateFV(start, monthly, rate, i)
});
}
This loop generates the data points needed for a line chart.