DEV Community

Cover image for I Built an Expense Tracker with Vanilla JavaScript (No Frameworks, No Libraries)
Praise Oluleye
Praise Oluleye

Posted on

I Built an Expense Tracker with Vanilla JavaScript (No Frameworks, No Libraries)

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,
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)