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

bing leobing leo
7 min read

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

  1. 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

  2. Flexible compounding: The calculator supports different compounding frequencies (monthly, quarterly, annually) by adjusting the n value in the formula.

  3. 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

  1. Mobile-first approach: Start with mobile layout, then enhance for larger screens

  2. Flexible grid system: Use CSS Grid for complex layouts with Tailwind utilities

  3. Adaptive spacing: Different padding and margins for different screen sizes

  4. 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:

  1. Solid foundations matter: Getting the core calculations right is essential

  2. User experience drives adoption: Interactive features and responsive design make the difference

  3. Modern tools enable rapid development: Next.js 15 and TypeScript provide an excellent developer experience

  4. 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.

0
Subscribe to my newsletter

Read articles from bing leo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

bing leo
bing leo