As part of my "one project a week" roadmap toward becoming a software engineer, this week I built an Expense Tracker — a clean, responsive app for logging income and expenses, with everything running client-side. No frameworks, no build tools. Just HTML, CSS, and JavaScript.
The Idea
I wanted something simple but genuinely useful: a place to log transactions, see totals update in real time, and have the data stick around after a page reload. That last part meant reaching for localStorage instead of just holding everything in a JS array that resets on refresh.
Features
Add a transaction with a description and an amount
Positive numbers = income, negative numbers = expenses
Auto-calculated Balance, Income, and Expense totals
Delete any transaction with one click
Data persists across reloads via localStorage
Fully responsive layout for tablet and mobile
Currency formatted as Nigerian Naira (₦) using Intl.NumberFormat
How It Works
Every transaction is stored as an object with an id, description, and amount:
js
transactions.push({
id: Date.now(),
description,
amount,
});
Totals are calculated with simple array methods — reduce for the running balance, and filter + reduce to separate income from expenses:
js
const balance = transactions.reduce((acc, t) => acc + t.amount, 0);
const income = transactions
.filter(t => t.amount > 0)
.reduce((acc, t) => acc + t.amount, 0);
const expense = transactions
.filter(t => t.amount < 0)
.reduce((acc, t) => acc + t.amount, 0);
For formatting currency, Intl.NumberFormat did all the heavy lifting instead of me hand-rolling a formatter:
js
function formatCurrency(number) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "NGN",
}).format(number);
}
What I Learned
localStorage is deceptively simple but easy to misuse. Since it only stores strings, every save/load needs JSON.stringify / JSON.parse. Forgetting either one is a classic bug.
Deriving state beats storing it. Balance, income, and expense are never stored directly — they're recalculated from the transactions array every time it changes. One source of truth, no sync bugs.
CSS Grid made the two-column layout painless — the transaction list and the form sit side-by-side on desktop and stack on mobile with a single media query change.
Small typos in CSS are silent killers. I had a few rgba() values with an extra channel argument that quietly broke shadows across the app — a good reminder to actually inspect rendered output, not just skim the code.
What's Next
A few things I want to add:
Transaction categories (Food, Transport, Salary, etc.)
Date/time stamps per transaction
Filtering and search
A simple chart to visualize spending by category
Export to CSV
Try It / Check the Code
The full source is on GitHub — feel free to fork it, break it, or suggest improvements. This is project N in my weekly build streak on the road to becoming a software engineer, so more of these are coming.
What would you add to a tracker like this? Let me know in the comments 👇
Top comments (0)