DEV Community

Profiterole
Profiterole

Posted on

How to Build a Static Site Calculator with Zero Dependencies

Building interactive tools for your static site doesn't require React, Vue, or any framework. A vanilla JS calculator can be just as powerful — and it'll load instantly, work offline, and never break due to a dependency update.

In this tutorial, we'll build a compound interest calculator from scratch: pure HTML, CSS, and JavaScript. No npm, no build step, no bundler.

Why Zero Dependencies?

  • No build tooling — open the file in a browser and it works
  • Instant load times — no framework overhead
  • Long-term stability — no npm audit nightmares in 2 years
  • Easy to embed in any static site (GitHub Pages, Netlify, etc.)

This approach is ideal for finance calculators, unit converters, and other utility tools where you just need clean interactivity.


What We're Building

A compound interest calculator that:

  • Takes principal, annual rate, compounding frequency, and years
  • Shows the final balance and total interest earned
  • Updates results in real-time as the user types
  • Looks clean on mobile and desktop

Step 1: The HTML Structure

Keep the markup semantic and accessible:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Compound Interest Calculator</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <main class="calculator">
    <h1>Compound Interest Calculator</h1>

    <div class="field">
      <label for="principal">Principal Amount ($)</label>
      <input type="number" id="principal" value="10000" min="0" step="100" />
    </div>

    <div class="field">
      <label for="rate">Annual Interest Rate (%)</label>
      <input type="number" id="rate" value="7" min="0" max="100" step="0.1" />
    </div>

    <div class="field">
      <label for="frequency">Compounding Frequency</label>
      <select id="frequency">
        <option value="1">Annually</option>
        <option value="4">Quarterly</option>
        <option value="12" selected>Monthly</option>
        <option value="365">Daily</option>
      </select>
    </div>

    <div class="field">
      <label for="years">Investment Period (Years)</label>
      <input type="number" id="years" value="10" min="1" max="50" step="1" />
    </div>

    <div class="results" id="results" aria-live="polite">
      <div class="result-item">
        <span class="label">Final Balance</span>
        <span class="value" id="final-balance"></span>
      </div>
      <div class="result-item">
        <span class="label">Total Interest Earned</span>
        <span class="value" id="total-interest"></span>
      </div>
      <div class="result-item">
        <span class="label">Effective Annual Rate</span>
        <span class="value" id="effective-rate"></span>
      </div>
    </div>
  </main>

  <script src="calculator.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Key accessibility notes:

  • aria-live="polite" on the results div means screen readers announce updates automatically
  • Each input has a proper <label> linked by for/id
  • Using semantic <main> instead of a generic <div>

Step 2: The CSS

Clean, responsive, and dependency-free:

/* style.css */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, -apple-system, sans-serif;
  background: #f5f7fa;
  color: #1a1a2e;
  padding: 2rem 1rem;
  min-height: 100vh;
}

.calculator {
  max-width: 480px;
  margin: 0 auto;
  background: #ffffff;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}

h1 {
  font-size: 1.5rem;
  margin-bottom: 1.5rem;
  color: #1a1a2e;
}

.field {
  margin-bottom: 1.25rem;
}

label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: #555;
  margin-bottom: 0.4rem;
}

input, select {
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 1.5px solid #d1d5db;
  border-radius: 8px;
  font-size: 1rem;
  color: #1a1a2e;
  background: #fff;
  transition: border-color 0.15s;
}

input:focus, select:focus {
  outline: none;
  border-color: #4f46e5;
}

.results {
  margin-top: 1.75rem;
  background: #f0f4ff;
  border-radius: 10px;
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.result-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.result-item .label {
  font-size: 0.875rem;
  color: #555;
}

.result-item .value {
  font-size: 1.1rem;
  font-weight: 700;
  color: #4f46e5;
}
Enter fullscreen mode Exit fullscreen mode

Using system-ui font means it looks native on every OS. The transition on focus gives tactile feedback without needing any JavaScript.


Step 3: The JavaScript Logic

This is where the real work happens. Keep it simple and reactive:

// calculator.js

// Grab all the inputs
const principalInput = document.getElementById('principal');
const rateInput = document.getElementById('rate');
const frequencyInput = document.getElementById('frequency');
const yearsInput = document.getElementById('years');

// Grab all the output elements
const finalBalanceEl = document.getElementById('final-balance');
const totalInterestEl = document.getElementById('total-interest');
const effectiveRateEl = document.getElementById('effective-rate');

/**
 * Core compound interest formula:
 * A = P * (1 + r/n)^(n*t)
 *
 * P = principal
 * r = annual rate as decimal
 * n = compounding frequency per year
 * t = time in years
 */
function calculateCompoundInterest(principal, annualRate, frequency, years) {
  const r = annualRate / 100;
  const n = frequency;
  const t = years;

  const finalBalance = principal * Math.pow(1 + r / n, n * t);
  const totalInterest = finalBalance - principal;

  // Effective Annual Rate: (1 + r/n)^n - 1
  const effectiveRate = (Math.pow(1 + r / n, n) - 1) * 100;

  return { finalBalance, totalInterest, effectiveRate };
}

// Format numbers as currency
function formatCurrency(amount) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: 2,
  }).format(amount);
}

// Format percentage
function formatPercent(value) {
  return value.toFixed(3) + '%';
}

// Read inputs, calculate, and update the DOM
function updateCalculator() {
  const principal = parseFloat(principalInput.value) || 0;
  const rate = parseFloat(rateInput.value) || 0;
  const frequency = parseInt(frequencyInput.value, 10) || 1;
  const years = parseFloat(yearsInput.value) || 1;

  // Guard against invalid inputs
  if (principal < 0 || rate < 0 || years < 0) {
    finalBalanceEl.textContent = 'Invalid input';
    totalInterestEl.textContent = '';
    effectiveRateEl.textContent = '';
    return;
  }

  const { finalBalance, totalInterest, effectiveRate } =
    calculateCompoundInterest(principal, rate, frequency, years);

  finalBalanceEl.textContent = formatCurrency(finalBalance);
  totalInterestEl.textContent = formatCurrency(totalInterest);
  effectiveRateEl.textContent = formatPercent(effectiveRate);
}

// Wire up real-time updates
[principalInput, rateInput, frequencyInput, yearsInput].forEach((input) => {
  input.addEventListener('input', updateCalculator);
});

// Run once on load to show default values
updateCalculator();
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  1. calculateCompoundInterest — pure function, no side effects. Easy to test.
  2. Intl.NumberFormat — browser-native currency formatting, no library needed.
  3. addEventListener('input', ...) — fires on every keystroke, so results feel instant.
  4. Guard clause — prevents showing NaN or nonsensical values.

Step 4: Deploy to GitHub Pages

You can deploy this as a standalone tool or embed it in any static site:

# From your repo root
git add .
git commit -m "Add compound interest calculator"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Then in your repo settings, enable GitHub Pages from the main branch. Done — live in ~30 seconds, free forever.


Taking It Further

Once you have this working, here are natural extensions:

Add a year-by-year table:

function buildGrowthTable(principal, rate, frequency, years) {
  const rows = [];
  for (let year = 1; year <= years; year++) {
    const { finalBalance } = calculateCompoundInterest(principal, rate, frequency, year);
    rows.push({ year, balance: finalBalance });
  }
  return rows;
}
Enter fullscreen mode Exit fullscreen mode

Persist inputs with localStorage:

function saveInputs() {
  localStorage.setItem('calc-principal', principalInput.value);
  // ... save others
}

function restoreInputs() {
  principalInput.value = localStorage.getItem('calc-principal') || '10000';
  // ... restore others
}
Enter fullscreen mode Exit fullscreen mode

URL-shareable state:

function saveToURL() {
  const params = new URLSearchParams({
    p: principalInput.value,
    r: rateInput.value,
    f: frequencyInput.value,
    y: yearsInput.value,
  });
  history.replaceState(null, '', '?' + params.toString());
}
Enter fullscreen mode Exit fullscreen mode

All of these use only browser APIs — still zero dependencies.


Wrapping Up

Here's the full pattern that makes this approach work:

Concern Solution
State Read directly from DOM inputs
Reactivity input event listeners
Formatting Intl.NumberFormat / Intl.DateTimeFormat
Persistence localStorage or URL params
Routing Hash fragments (#)

For a large application you'd reach for a framework. But for a calculator, a converter, or a small decision tool? Vanilla JS is faster to build, faster to load, and easier to maintain.

If you're building tools for a specific audience — like step-by-step guides for navigating bureaucracy — keeping dependencies at zero means the tool will still work perfectly in 5 years.


Built something with this pattern? Drop a link in the comments — I'd love to see it.

Top comments (0)