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 auditnightmares 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>
Key accessibility notes:
-
aria-live="polite"on the results div means screen readers announce updates automatically - Each input has a proper
<label>linked byfor/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;
}
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();
What's happening here:
-
calculateCompoundInterest— pure function, no side effects. Easy to test. -
Intl.NumberFormat— browser-native currency formatting, no library needed. -
addEventListener('input', ...)— fires on every keystroke, so results feel instant. -
Guard clause — prevents showing
NaNor 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
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;
}
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
}
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());
}
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)