What you will build

This project lets you choose a month, enter an expense description and amount, and assign a category. It then shows the entries for the selected month, a total for each category, and an overall total. Changing the month changes the view; it does not move or delete entries.

You need a desktop browser with Developer Tools and support for month inputs, such as a recent version of Chrome or Edge. No packages or server are required. The example uses US dollars, but you can change the currency setting later. To keep the first version focused, entries live only in memory and disappear when you reload or close the tab.

Run the project in a browser

Open a blank browser tab, then open Developer Tools and find the Console. Read the code before running it, as you should with any code pasted into a browser console. Copy the complete JavaScript below and run it once. It creates the form and results directly on the page, so there is no separate HTML file to prepare.

{ const categories = ["Food", "Transport", "Housing", "Other"]; const expenses = []; const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }); const make = tag => document.createElement(tag); const labelFor = (text, input) => { const label = make("label"); label.textContent = text + " "; label.append(input); return label; }; const app = make("main"); const title = make("h1"); title.textContent = "Monthly expense tracker"; const form = make("form"); const month = make("input"); month.type = "month"; month.required = true; const today = new Date(); month.value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`; const category = make("select"); for (const name of categories) { const option = make("option"); option.value = name; option.textContent = name; category.append(option); } const description = make("input"); description.type = "text"; description.required = true; description.maxLength = 80; const amount = make("input"); amount.type = "text"; amount.inputMode = "decimal"; amount.placeholder = "12.50"; amount.required = true; const add = make("button"); add.type = "submit"; add.textContent = "Add expense"; form.append(labelFor("Month:", month), labelFor("Category:", category), labelFor("Description:", description), labelFor("USD:", amount), add); const message = make("p"); const summary = make("section"); const entries = make("section"); app.append(title, form, message, summary, entries); document.body.append(app); function render() { const visible = expenses.filter(item => item.month === month.value); summary.replaceChildren(); entries.replaceChildren(); const heading = make("h2"); heading.textContent = "Category totals"; summary.append(heading); for (const name of categories) { const cents = visible.filter(item => item.category === name) .reduce((sum, item) => sum + item.cents, 0); const line = make("p"); line.textContent = `${name}: ${money.format(cents / 100)}`; summary.append(line); } const total = visible.reduce((sum, item) => sum + item.cents, 0); const overall = make("p"); overall.textContent = `Overall: ${money.format(total / 100)}`; summary.append(overall); const listHeading = make("h2"); listHeading.textContent = "Entries"; const list = make("ul"); for (const item of visible) { const row = make("li"); row.textContent = `${item.description} — ${item.category}: ${money.format(item.cents / 100)}`; list.append(row); } entries.append(listHeading, list); } form.addEventListener("submit", event => { event.preventDefault(); const raw = amount.value.trim(); if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(raw)) { message.textContent = "Enter a positive amount with at most two decimal places."; return; } const [dollars, fraction = ""] = raw.split("."); const cents = Number(dollars) * 100 + Number(fraction.padEnd(2, "0")); if (!Number.isSafeInteger(cents) || cents <= 0) { message.textContent = "Enter a positive amount within the supported range."; return; } expenses.push({ month: month.value, category: category.value, description: description.value.trim(), cents }); description.value = ""; amount.value = ""; message.textContent = "Expense added."; render(); }); month.addEventListener("change", render); render(); }

How the totals work

Each expense stores the month selected when it was added. The render function first filters the array to that month, then adds the amounts for each category. The overall total uses the same filtered entries, so an expense in another month cannot affect it.

Amounts are converted to whole cents before they enter the array. For example, 12.50 becomes 1250 cents. This avoids repeated decimal arithmetic when adding ordinary currency amounts. Formatting happens only when a value is displayed. The code also rejects zero, negative amounts, more than two decimal places, and values outside JavaScript’s safe-integer range.

Check the behavior and choose a next step

Try adding Food expenses of 12.50 and 3.25, then a Transport expense of 4.00. Food should show $15.75, Transport $4.00, and Overall $19.75. Switch to another month: its totals should be zero. Switch back: the three entries should still be there. Also try 0 and 1.234 to check the validation message.

A useful next step is a delete button beside each entry. Give each expense an identifier, find the corresponding entry when its button is clicked, remove it from the array, and call render again. If you later add browser storage, save and restore the entries deliberately, validate restored data, and explain to users that storage on one browser is not a backup or a way to sync devices.

Back to category ↗