Building a Modern Compound Interest Calculator with Next.js 15 and TypeScript

How I built a comprehensive financial tool with real-time calculations, interactive charts, and a beautiful UI
Introduction
Financial literacy is more important than ever, and compound interest is one of the most powerful concepts for building wealth. I recently built a comprehensive compound interest calculator that not only performs accurate calculations but also provides an engaging user experience with interactive charts and detailed breakdowns.
In this article, I'll walk you through the technical decisions, implementation details, and lessons learned while building this project with Next.js 15, TypeScript, and modern web technologies.
๐ compound interest calculator
Tech Stack Overview
Here's what powers this application:
Framework: Next.js 15 with App Router
Language: TypeScript for type safety
Styling: Tailwind CSS with custom animations
Charts: Recharts for interactive visualizations
UI Components: Radix UI primitives
State Management: React hooks with custom logic
Internationalization: next-intl for multi-language support
Deployment: Vercel with automatic deployments
Key Features
Before diving into the technical implementation, let me highlight what makes this calculator special:
โจ Real-time calculations as you type
๐ Interactive charts (line charts and pie charts)
๐ฑ Responsive design that works on all devices
๐ Internationalization support
๐ Year-by-year breakdown with detailed tables
๐พ Export functionality (CSV and chart images)
๐จ Modern UI with smooth animations
๐งฎ Inflation adjustment calculations
The Core Calculation Logic
The heart of any compound interest calculator is the mathematical formula. Here's how I implemented it:
const calculateCompoundInterest = useCallback(() => {
const n = getCompoundingPeriodsPerYear(compoundingFrequency);
const contributionPeriods = getContributionPeriodsPerYear(contributionFrequency);
const r = annualRate / 100;
let currentBalance = initialAmount;
const yearlyBreakdown = [];
let totalContributions = initialAmount;
for (let year = 1; year <= years; year++) {
const startBalance = currentBalance;
const yearlyContributions = contributionAmount * contributionPeriods;
// Calculate compound interest for the year
const interestEarned = currentBalance * (Math.pow(1 + r / n, n) - 1);
currentBalance += interestEarned;
currentBalance += yearlyContributions;
totalContributions += yearlyContributions;
const interest = currentBalance - startBalance - yearlyContributions;
// Calculate inflation-adjusted value
const inflationAdjustedBalance = currentBalance / Math.pow(1 + inflationRate / 100, year);
yearlyBreakdown.push({
year,
startBalance,
contributions: yearlyContributions,
interest,
endBalance: currentBalance,
inflationAdjustedBalance
});
}
// ... rest of calculation logic
}, [initialAmount, contributionAmount, contributionFrequency, annualRate, compoundingFrequency, years, inflationRate]);
Why This Approach Works
Year-by-year calculation: Instead of using the standard compound interest formula directly, I calculate each year individually. This allows for:
More accurate handling of regular contributions
Detailed breakdown for each year
Flexibility for different contribution frequencies
Flexible compounding: The calculator supports different compounding frequencies (monthly, quarterly, annually) by adjusting the
n
value in the formula.Real-time updates: Using
useCallback
with proper dependencies ensures calculations update immediately when any input changes.
State Management Strategy
Rather than reaching for external state management libraries, I used React's built-in hooks effectively:
const [initialAmount, setInitialAmount] = useState<number>(5000);
const [contributionAmount, setContributionAmount] = useState<number>(150);
const [contributionFrequency, setContributionFrequency] = useState<'monthly' | 'quarterly' | 'annually'>('monthly');
const [annualRate, setAnnualRate] = useState<number>(7);
const [compoundingFrequency, setCompoundingFrequency] = useState<'monthly' | 'quarterly' | 'annually'>('monthly');
const [years, setYears] = useState<number>(10);
const [calculation, setCalculation] = useState<CompoundCalculation | null>(null);
TypeScript Interfaces for Type Safety
I defined clear interfaces to ensure type safety throughout the application:
interface CompoundCalculation {
futureValue: number;
totalContributions: number;
totalInterest: number;
inflationAdjustedValue: number;
yearlyBreakdown: Array<{
year: number;
startBalance: number;
contributions: number;
interest: number;
endBalance: number;
inflationAdjustedBalance: number;
}>;
}
This approach provides excellent IntelliSense support and catches potential errors at compile time.
Interactive Charts with Recharts
One of the most engaging features is the interactive chart system. I implemented both line charts and pie charts with custom interactions:
const getChartData = () => {
if (!calculation) return [];
return calculation.yearlyBreakdown.map((item) => ({
year: item.year,
'Total Balance': showInflationAdjusted ? item.inflationAdjustedBalance : item.endBalance,
'Interest Earned': showInflationAdjusted
? item.interest / Math.pow(1 + inflationRate / 100, item.year)
: item.interest,
'Contributions': showInflationAdjusted
? (contributionAmount * getContributionPeriodsPerYear(contributionFrequency)) / Math.pow(1 + inflationRate / 100, item.year)
: item.contributions
}));
};
// Interactive legend functionality
const toggleLineVisibility = (dataKey: string) => {
setHiddenLines(prev => {
const newSet = new Set(prev);
if (newSet.has(dataKey)) {
newSet.delete(dataKey);
} else {
newSet.add(dataKey);
}
return newSet;
});
};
Chart Features
Toggle visibility: Click legend items to show/hide data series
Responsive design: Charts adapt to different screen sizes
Export functionality: Download charts as PNG images
Inflation adjustment: Toggle between nominal and real values
Responsive Design with Tailwind CSS
The calculator works seamlessly across all device sizes. Here's how I achieved this:
<div className="flex flex-col lg:grid lg:grid-cols-12 gap-4 lg:gap-6 lg:items-stretch">
{/* Input Section */}
<div className="lg:col-span-4">
<div className="bg-white rounded-xl border border-slate-200 shadow-lg p-4 lg:p-6 backdrop-blur-sm h-full flex flex-col">
{/* Input controls */}
</div>
</div>
{/* Results Section */}
<div className="lg:col-span-8">
{/* Charts and tables */}
</div>
</div>
Key Responsive Strategies
Mobile-first approach: Start with mobile layout, then enhance for larger screens
Flexible grid system: Use CSS Grid for complex layouts with Tailwind utilities
Adaptive spacing: Different padding and margins for different screen sizes
Touch-friendly controls: Larger buttons and inputs on mobile devices
Export Functionality
Users can export their calculations in multiple formats:
CSV Export
const downloadCSV = () => {
if (!calculation) return;
const headers = ['Year', 'Start Balance', 'Contributions', 'Interest', 'End Balance'];
const csvContent = [
headers.join(','),
...calculation.yearlyBreakdown.map(row => [
row.year,
row.startBalance.toFixed(2),
row.contributions.toFixed(2),
row.interest.toFixed(2),
row.endBalance.toFixed(2)
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `compound-interest-breakdown-${years}years.csv`);
link.click();
};
Chart Image Export
const downloadChartImage = async (chartType: 'line' | 'pie') => {
const element = chartType === 'line' ? chartRef.current : pieChartRef.current;
if (!element) return;
try {
const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(element, {
backgroundColor: '#ffffff',
scale: 2,
useCORS: true,
allowTaint: true
});
const link = document.createElement('a');
link.download = `compound-interest-${chartType}-chart.png`;
link.href = canvas.toDataURL();
link.click();
} catch (error) {
console.error('Error downloading chart:', error);
}
};
Internationalization with next-intl
Supporting multiple languages was crucial for reaching a global audience:
// i18n/routing.ts
export const routing = defineRouting({
locales: ['en', 'zh', 'es', 'fr'],
defaultLocale: 'en'
});
// Component usage
const t = useTranslations('Calculator');
return (
<label htmlFor="initialAmount">
{t('inputs.initialAmount')}
</label>
);
This setup allows for easy translation management and automatic locale detection.
Performance Optimizations
1. Memoized Calculations
const calculateCompoundInterest = useCallback(() => {
// Expensive calculation logic
}, [initialAmount, contributionAmount, contributionFrequency, annualRate, compoundingFrequency, years, inflationRate]);
2. Dynamic Imports
const html2canvas = (await import('html2canvas')).default;
3. Optimized Re-renders
Using proper dependency arrays and avoiding unnecessary state updates.
Lessons Learned
1. Start with the Math
Get the core calculations right first. Everything else builds on this foundation.
2. TypeScript is Worth It
The type safety caught numerous bugs during development and made refactoring much safer.
3. Mobile-First Design
Starting with mobile constraints led to a cleaner, more focused design.
4. User Experience Matters
Features like real-time updates and export functionality significantly improve user engagement.
5. Performance Considerations
Even simple calculations can become expensive when running on every keystroke. Proper memoization is crucial.
Future Enhancements
Here are some features I'm considering for future versions:
๐ Comparison mode: Compare different investment scenarios side-by-side
๐ More chart types: Add bar charts and area charts
๐ฐ Tax calculations: Include tax implications in the calculations
๐ฏ Goal-based planning: Calculate required contributions to reach a target amount
๐ฑ PWA support: Make it installable as a mobile app
๐ Shareable links: Generate URLs with pre-filled parameters
Conclusion
Building this compound interest calculator was an excellent exercise in combining mathematical precision with modern web development practices. The project demonstrates how Next.js 15, TypeScript, and Tailwind CSS can work together to create a professional, user-friendly financial tool.
The key takeaways from this project:
Solid foundations matter: Getting the core calculations right is essential
User experience drives adoption: Interactive features and responsive design make the difference
Modern tools enable rapid development: Next.js 15 and TypeScript provide an excellent developer experience
Performance and accessibility: These should be considered from the start, not added later
Whether you're building financial tools or any other type of calculator, I hope this breakdown gives you some useful insights and inspiration for your own projects!
What would you like to see in a financial calculator? Let me know in the comments below!
If you found this article helpful, please give it a โค๏ธ and follow me for more web development content.
Subscribe to my newsletter
Read articles from bing leo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
