Create Expense Tracker App with HTML, CSS & JavaScript

Faraz

By Faraz -

Learn how to create a simple Expense Tracker App using HTML, Tailwind CSS, and JavaScript. Step-by-step guide for beginners to manage spending easily.


create-expense-tracker-app-with-html-css-and-javascript.webp

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Conclusion
  6. Preview

Managing money is important, and creating your own Expense Tracker App can help you stay organized. This project is a great way to learn HTML, Tailwind CSS, and JavaScript. Even if you're just starting out in web development, you can follow this easy guide and build a real-world app to track income and expenses.

In this blog, you’ll learn how to:

  • Build a responsive UI using HTML and Tailwind CSS
  • Store and retrieve data using JavaScript and localStorage
  • Add interactivity with modals, charts, and filters

Let’s get started!

Prerequisites

Before starting, make sure you know the basics of:

  • HTML: Creating forms, tables, and buttons
  • CSS/Tailwind CSS: Styling layouts and UI elements
  • JavaScript: Working with DOM, arrays, and events

Also, make sure you have:

  • A code editor like VS Code
  • A browser (like Chrome) to test the app

Source Code

Step 1 (HTML Code):

To get started, we first need to create a basic HTML file. In this file, we will include the main structure for our expense-tracker app. Let's break down the HTML code step by step:

1. HTML Boilerplate & Head Section

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FinTrack - Expense Tracker App</title>

    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>

    <!-- Google Fonts: Inter -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="styles.css">
</head>
  • <!DOCTYPE html>: Declares HTML5 document.
  • <html lang="en">: Sets English as the page language.
  • <meta charset="UTF-8">: Defines character encoding for proper text rendering.
  • <meta name="viewport"...>: Ensures responsiveness on mobile devices.
  • <title>: Sets the browser tab title to “FinTrack - Expense Tracker App”.
  • Tailwind CSS: A utility-first CSS framework for styling.
  • Google Fonts (Inter): Loads the Inter font family for typography.
  • Chart.js: Used for drawing dynamic charts.
  • styles.css: Optional custom CSS file.

2. Body Tag with Default Styling

<body class="text-gray-800">
  • Applies a dark gray text color using Tailwind classes.

3. Top Navigation Bar

<nav class="bg-white shadow-sm sticky top-0 z-40">...</nav>
  • Sticky, white navigation bar with shadow.
  • Contains logo (SVG) and app name "FinTrack".
  • sticky top-0 z-40: Fixes navbar to the top during scroll.

4. Main Content Section

<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">...</main>
  • Sets the content area with padding and a centered layout.
  • Uses Tailwind's responsive spacing utilities.

5. Dashboard Overview

<section id="dashboard">...</section>
  • Shows 3 stats cards:
    • Total Balance
    • Total Income
    • Total Expenses
  • Each card is styled with shadow, padding, and colored values (text-indigo-600, text-green-500, text-red-500).

6. Analytics Section with Charts

<section id="analytics">...</section>
  • Displays two charts using Chart.js:
    • Spending by Category (Pie Chart)
    • Income vs Expense Trend (Line or Bar Chart)
  • Layout adjusts on large screens using grid-cols-2.

7. Transaction History Section

<section id="history">...</section>
  • Contains:
    • Search input and sort dropdown
    • Table for listing transactions
    • Empty state message when no transactions exist
  • Search helps users find transactions by keyword.
  • Sorting options include date and amount (ascending/descending).

8. Footer with Clear All Button

<footer class="text-center mt-12">
    <button id="clearAllBtn">Clear All Data</button>
</footer>
  • A button to delete all data.

9. Floating Action Button (FAB)

<button id="openModalBtn" class="fixed bottom-6 right-6 ...">+</button>
  • Circular button placed at the bottom-right of the screen.
  • Used to open the “Add Transaction” modal.
  • Uses hover/transition effects and an SVG plus icon.

10. Add/Edit Transaction Modal

<div id="transactionModal" class="...">
    <form id="transactionForm">...</form>
</div>
  • Modal pop-up for adding/editing transactions.
  • Contains fields:
    • Title, Amount, Type (income/expense), Category, Date
  • Form submission handled via JavaScript.

11. Delete Confirmation Modal

<div id="confirmModal" class="...">
    <div>...</div>
</div>
  • Confirmation modal before deleting a transaction.
  • Two buttons: Cancel and Confirm Delete.

12. JavaScript File Inclusion

<script src="script.js"></script>
</body>
</html>
  • External script file to handle dynamic behavior like:
    • Chart rendering
    • Modal control
    • Form validation
    • Data storage

Step 2 (CSS Code):

Once the basic HTML structure of the expense tracker app is in place, the next step is to add styling to the app using CSS. Let's break down the CSS code step by step:

1. Body Styling

body {
  font-family: 'Inter', sans-serif;
  background-color: #f8fafc; /* Tailwind's gray-50 */
}
  • font-family: 'Inter', sans-serif;: Sets the primary font to Inter with sans-serif fallback.
  • background-color: #f8fafc;: Applies a very light gray background similar to Tailwind’s gray-50.

2. Custom Scrollbar Styling (WebKit Browsers Only)

::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
  • ::-webkit-scrollbar: Targets the whole scrollbar (only works in WebKit browsers like Chrome, Safari).
    • width: 8px;: Sets scrollbar width.
  • ::-webkit-scrollbar-track: Background of the scrollbar track.
    • #f1f5f9: Light gray tone (Tailwind gray-100).
  • ::-webkit-scrollbar-thumb: The draggable scrollbar handle.
    • #cbd5e1: Slightly darker gray (Tailwind gray-300).
    • border-radius: 10px: Makes the handle rounded.
  • ::-webkit-scrollbar-thumb:hover: Color change on hover for visual feedback.
    • #94a3b8: Darker shade on hover (Tailwind gray-400).

3. Modal Backdrop & Animation

.modal-backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  transition: opacity 0.3s ease-out;
}
.modal-content {
  transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
.modal-hidden {
  opacity: 0;
}
.modal-hidden .modal-content {
  transform: scale(0.95);
  opacity: 0;
}
  • .modal-backdrop:
    • Semi-transparent black background (rgba(0,0,0,0.5)) behind modal.
    • Smooth fade effect via transition: opacity.
  • .modal-content:
    • Applies transitions for scaling and fading effects when showing or hiding modals.
  • .modal-hidden:
    • Hides the modal with opacity: 0.
  • .modal-hidden .modal-content:
    • Shrinks and fades out the modal content (scale down to 95% and reduce opacity).

4. Row Deletion Animation

.row-deleting {
  transition: opacity 0.3s ease-out, transform 0.3s ease-out;
  opacity: 0;
  transform: scale(0.95);
}
  • Used when a transaction row is being deleted.
  • Smoothly fades and shrinks the row before removal.
  • opacity: 0 and scale(0.95) create a disappearing effect.

5. Floating Action Button (FAB) Gradient

.fab-gradient {
  background-image: linear-gradient(to right, #4f46e5, #6366f1);
}
  • Applies a horizontal gradient from #4f46e5 to #6366f1 (both shades of indigo).
  • Creates a stylish button look used for the floating action button.
body {
  font-family: 'Inter', sans-serif;
  background-color: #f8fafc; /* Tailwind's gray-50 */
}
/* Custom scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }

/* Modal backdrop and content animation */
.modal-backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  transition: opacity 0.3s ease-out;
}
.modal-content {
  transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
.modal-hidden {
  opacity: 0;
}
.modal-hidden .modal-content {
  transform: scale(0.95);
  opacity: 0;
}

/* Row deletion animation */
.row-deleting {
  transition: opacity 0.3s ease-out, transform 0.3s ease-out;
  opacity: 0;
  transform: scale(0.95);
}
.fab-gradient {
  background-image: linear-gradient(to right, #4f46e5, #6366f1);
} 

Step 3 (JavaScript Code):

Finally, we need to create a function in JavaScript. Let's break down the JavaScript code step by step:

1. Initial Setup on Page Load

document.addEventListener('DOMContentLoaded', () => { ... });
  • Ensures the script runs only after the full DOM is ready.
  • Prevents errors from trying to access DOM elements before they're available.

2. Element References

const totalBalanceEl = document.getElementById('totalBalance');
const openModalBtn = document.getElementById('openModalBtn');
// ...more
  • Stores references to important DOM elements like balance display, modal buttons, form, etc.
  • Used throughout the code for updates and interaction.

3. State & Local Storage

let transactions = JSON.parse(localStorage.getItem('transactions')) || [];
  • Retrieves saved transactions from local storage.
  • If none found, initializes as an empty array.

4. Utility Functions

Format currency and date:

const formatCurrency = (amount) => new Intl.NumberFormat(...).format(amount);
const formatDate = (dateString) => new Date(dateString).toLocaleDateString(...);
  • Makes numbers readable as USD currency.
  • Converts raw date to a readable format.

5. Dashboard Rendering

const updateDashboard = () => {
  const income = ...
  const expenses = ...
  const balance = income - expenses;
  animateValue(...);
}
  • Calculates totals for income, expenses, and balance.
  • Animates number updates for a smooth visual effect.

6. Transaction List Rendering

const renderTransactionList = () => {
  // Filters, sorts, and shows transaction rows
}
  • Filters transactions by search term.
  • Sorts by date or amount.
  • Shows “No transaction” message if empty.
  • Uses createTransactionRow() to build each row.

7. Create Transaction Row

const createTransactionRow = (transaction) => { ... };
  • Builds a table row for each transaction.
  • Includes edit & delete buttons with icons.
  • Adds income/expense signs and styling.

8. Chart Rendering

const renderCharts = () => {
  renderCategoryPieChart();
  renderIncomeVsExpenseChart();
};
  • Uses Chart.js to draw:
    • Doughnut chart for expenses by category.
    • Bar chart for monthly income vs expense trends.
  • If no data exists, shows empty message inside chart container.

9. Modals

const openModal = (el) => el.classList.remove('modal-hidden');
const closeModal = (el) => { ... };
  • Opens and closes modals by toggling classes.
  • Used for:
    • Add/Edit Transaction modal
    • Confirmation modal (e.g., for delete)

10. Open Transaction Modal

const openTransactionModal = (transaction = null) => { ... };
  • If a transaction is passed, it opens in edit mode.
  • Else opens in add new mode.
  • Fills form fields accordingly.

11. Confirmation Modal

const openConfirmModal = (config) => { ... };
  • Custom modal that asks the user to confirm deletion or clearing.
  • Executes a callback (onConfirm) if confirmed.

12. Save to LocalStorage

const saveTransactions = () =>
  localStorage.setItem('transactions', JSON.stringify(transactions));
  • Updates the localStorage with the latest transaction list.

13. Form Submit Handler

const handleFormSubmit = (e) => { ... };
  • Prevents page refresh on form submission.
  • If editing: updates existing transaction.
  • If adding: creates a new one with a unique ID.
  • Then saves and rerenders.

14. Edit & Delete Button Handler

const handleListClick = (e) => { ... };
  • Detects if the user clicked edit or delete inside a transaction row.
  • Edit: Opens modal with existing values.
  • Delete: Opens a confirmation modal and removes the row on confirmation.

15. Clear All Data

const handleClearAll = () => { ... };
  • Clears all transactions after user confirmation.
  • Updates local storage and re-renders.

16. Initial Setup & Event Listeners

const init = () => {
  // Event listeners
  // Load default transactions if first time
};
  • Sets up:
  • Default transactions (demo data) if localStorage is empty.
  • Event listeners for:
    • Open/Close modals
    • Submit form
    • Search/sort change
    • Clear all

17. Call init()

init();
  • Kicks off the whole app once the DOM is ready.
document.addEventListener('DOMContentLoaded', () => {
  // --- DOM Element References ---
  const totalBalanceEl = document.getElementById('totalBalance');
  const totalIncomeEl = document.getElementById('totalIncome');
  const totalExpensesEl = document.getElementById('totalExpenses');
  const transactionListEl = document.getElementById('transactionList');
  const emptyStateContainer = document.getElementById('empty-state-container');

  const openModalBtn = document.getElementById('openModalBtn');
  const closeModalBtn = document.getElementById('closeModalBtn');
  const transactionModal = document.getElementById('transactionModal');
  const transactionForm = document.getElementById('transactionForm');
  const modalTitle = document.getElementById('modalTitle');

  const confirmModal = document.getElementById('confirmModal');
  const confirmModalTitle = document.getElementById('confirmModalTitle');
  const confirmModalText = document.getElementById('confirmModalText');
  const cancelConfirmBtn = document.getElementById('cancelConfirmBtn');

  const clearAllBtn = document.getElementById('clearAllBtn');
  const searchInput = document.getElementById('searchInput');
  const sortSelect = document.getElementById('sortSelect');

  // --- Chart References ---
  let categoryPieChart;
  let incomeVsExpenseChart;

  // --- State Management ---
  let transactions = JSON.parse(localStorage.getItem('transactions')) || [];

  // --- Utility Functions ---
  const formatCurrency = (amount) =>
    new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(amount);
  const formatDate = (dateString) =>
    new Date(dateString).toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      timeZone: 'UTC',
    });

  // --- Main Render Function ---
  const render = () => {
    updateDashboard();
    renderTransactionList();
    renderCharts();
  };

  // --- Animated Number Counter ---
  const animateValue = (element, start, end, duration) => {
    let startTimestamp = null;
    const step = (timestamp) => {
      if (!startTimestamp) startTimestamp = timestamp;
      const progress = Math.min((timestamp - startTimestamp) / duration, 1);
      const currentValue = progress * (end - start) + start;
      element.textContent = formatCurrency(currentValue);
      if (progress < 1) {
        window.requestAnimationFrame(step);
      }
    };
    window.requestAnimationFrame(step);
  };

  // --- Dashboard Update ---
  const updateDashboard = () => {
    const income = transactions
      .filter((t) => t.type === 'income')
      .reduce((sum, t) => sum + t.amount, 0);
    const expenses = transactions
      .filter((t) => t.type === 'expense')
      .reduce((sum, t) => sum + t.amount, 0);
    const balance = income - expenses;

    const getNumericValue = (el) =>
      parseFloat(el.textContent.replace(/[^0-9.-]+/g, '')) || 0;

    animateValue(totalBalanceEl, getNumericValue(totalBalanceEl), balance, 500);
    animateValue(totalIncomeEl, getNumericValue(totalIncomeEl), income, 500);
    animateValue(
      totalExpensesEl,
      getNumericValue(totalExpensesEl),
      expenses,
      500
    );
  };

  // --- Transaction List Rendering ---
  const renderTransactionList = () => {
    transactionListEl.innerHTML = '';
    const searchTerm = searchInput.value.toLowerCase();
    const sortValue = sortSelect.value;

    let filteredTransactions = transactions.filter(
      (t) =>
        (t.title ?? '').toLowerCase().includes(searchTerm) ||
    (t.category ?? '').toLowerCase().includes(searchTerm)
    );
    filteredTransactions.sort((a, b) => {
      switch (sortValue) {
        case 'date_asc':
          return new Date(a.date) - new Date(b.date);
        case 'amount_desc':
          return b.amount - a.amount;
        case 'amount_asc':
          return a.amount - b.amount;
        default:
          return new Date(b.date) - new Date(a.date);
      }
    });

    if (transactions.length === 0) {
      emptyStateContainer.classList.remove('hidden');
    } else {
      emptyStateContainer.classList.add('hidden');
      if (filteredTransactions.length === 0) {
        const row = document.createElement('tr');
        row.innerHTML = `<td colspan="4" class="text-center py-10 text-gray-500">No transactions match your search.</td>`;
        transactionListEl.appendChild(row);
      } else {
        filteredTransactions.forEach((transaction) => {
          const row = createTransactionRow(transaction);
          transactionListEl.appendChild(row);
        });
      }
    }
  };

  const createTransactionRow = (transaction) => {
    const row = document.createElement('tr');
    row.className = 'hover:bg-gray-50';
    row.dataset.id = transaction.id;
    const amountColor =
      transaction.type === 'income' ? 'text-green-600' : 'text-red-600';
    const amountSign = transaction.type === 'income' ? '+' : '-';
    row.innerHTML = `
    <td class="px-6 py-4 whitespace-nowrap"><div class="text-sm font-semibold text-gray-900">${
      transaction.title
    }</div><div class="text-xs text-gray-500">${
transaction.category
}</div></td>
    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatDate(
      transaction.date
    )}</td>
    <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-right ${amountColor}">${amountSign} ${formatCurrency(
transaction.amount
)}</td>
    <td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
        <button class="edit-btn text-indigo-600 hover:text-indigo-900 mr-3 transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.5L14.732 3.732z"></path></svg></button>
        <button class="delete-btn text-red-600 hover:text-red-900 transition-colors"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></button>
    </td>`;
    return row;
  };

  // --- Chart Rendering ---
  const renderCharts = () => {
    renderCategoryPieChart();
    renderIncomeVsExpenseChart();
  };

  const createEmptyChartState = (container, message) => {
    container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-center text-gray-500">
    <svg class="h-12 w-12 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h12M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-1.5m-6-3.75h.008v.008H9v-.008zm4.5 0h.008v.008h-.008v-.008zm2.25 0h.008v.008h-.008v-.008zM3.75 21h16.5M3.75 21v-1.5m16.5 1.5v-1.5" /></svg>
    <p class="mt-2 text-sm">${message}</p>
</div>`;
  };

  const renderCategoryPieChart = () => {
    const container = document.getElementById('categoryChartContainer');
    if (categoryPieChart) categoryPieChart.destroy();

    const expenseData = transactions
      .filter((t) => t.type === 'expense')
      .reduce((acc, t) => {
        acc[t.category] = (acc[t.category] || 0) + t.amount;
        return acc;
      }, {});

    if (Object.keys(expenseData).length === 0) {
      createEmptyChartState(container, 'No expense data to display.');
      return;
    }
    container.innerHTML = '<canvas id="categoryPieChart"></canvas>';
    const ctx = document.getElementById('categoryPieChart').getContext('2d');
    categoryPieChart = new Chart(ctx, {
      type: 'doughnut',
      data: {
        labels: Object.keys(expenseData),
        datasets: [
          {
            data: Object.values(expenseData),
            backgroundColor: [
              '#4f46e5',
              '#10b981',
              '#ef4444',
              '#f97316',
              '#3b82f6',
              '#8b5cf6',
              '#ec4899',
            ],
            borderColor: '#ffffff',
            borderWidth: 2,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { position: 'bottom', labels: { font: { family: 'Inter' } } },
        },
      },
    });
  };

  const renderIncomeVsExpenseChart = () => {
    const container = document.getElementById('trendChartContainer');
    if (incomeVsExpenseChart) incomeVsExpenseChart.destroy();

    const monthlyData = transactions.reduce((acc, t) => {
      const month = new Date(t.date).toLocaleString('default', {
        month: 'short',
        year: '2-digit',
      });
      if (!acc[month]) acc[month] = { income: 0, expense: 0 };
      acc[month][t.type] += t.amount;
      return acc;
    }, {});

    const sortedMonths = Object.keys(monthlyData).sort(
      (a, b) => new Date(`01 ${a}`) - new Date(`01 ${b}`)
    );
    if (sortedMonths.length === 0) {
      createEmptyChartState(container, 'No data for trend analysis.');
      return;
    }
    container.innerHTML = '<canvas id="incomeVsExpenseChart"></canvas>';
    const ctx = document
      .getElementById('incomeVsExpenseChart')
      .getContext('2d');
    incomeVsExpenseChart = new Chart(ctx, {
      type: 'bar',
      data: {
        labels: sortedMonths,
        datasets: [
          {
            label: 'Income',
            data: sortedMonths.map((m) => monthlyData[m].income),
            backgroundColor: 'rgba(16, 185, 129, 0.6)',
            borderColor: 'rgba(5, 150, 105, 1)',
            borderWidth: 1,
          },
          {
            label: 'Expense',
            data: sortedMonths.map((m) => monthlyData[m].expense),
            backgroundColor: 'rgba(239, 68, 68, 0.6)',
            borderColor: 'rgba(220, 38, 38, 1)',
            borderWidth: 1,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          y: {
            beginAtZero: true,
            ticks: { callback: (v) => formatCurrency(v) },
          },
        },
        plugins: { legend: { position: 'top' } },
      },
    });
  };

  // --- Modal Handling ---
  const openModal = (modalEl) => {
    modalEl.classList.remove('modal-hidden', 'pointer-events-none');
  };
  const closeModal = (modalEl) => {
    modalEl.classList.add('modal-hidden');
    setTimeout(() => modalEl.classList.add('pointer-events-none'), 300);
  };

  const openTransactionModal = (transaction = null) => {
    transactionForm.reset();
    if (transaction) {
      modalTitle.textContent = 'Edit Transaction';
      transactionForm.querySelector('button[type="submit"]').textContent =
        'Save Changes';
      document.getElementById('transactionId').value = transaction.id;
      document.getElementById('title').value = transaction.title;
      document.getElementById('amount').value = transaction.amount;
      document.querySelector(
        `input[name="type"][value="${transaction.type}"]`
      ).checked = true;
      document.getElementById('category').value = transaction.category;
      document.getElementById('date').value = transaction.date;
    } else {
      modalTitle.textContent = 'Add Transaction';
      transactionForm.querySelector('button[type="submit"]').textContent =
        'Add Transaction';
      document.getElementById('transactionId').value = '';
      document.getElementById('date').valueAsDate = new Date();
    }
    openModal(transactionModal);
  };

  // --- Confirmation Modal Logic ---
  const openConfirmModal = (config) => {
    confirmModalTitle.textContent = config.title;
    confirmModalText.textContent = config.text;
    openModal(confirmModal);

    // Get the button from the DOM each time to avoid stale references from node replacement
    let confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
    const newConfirmBtn = confirmDeleteBtn.cloneNode(true);
    confirmDeleteBtn.parentNode.replaceChild(newConfirmBtn, confirmDeleteBtn);

    newConfirmBtn.addEventListener(
      'click',
      () => {
        config.onConfirm();
        closeModal(confirmModal);
      },
      { once: true }
    );
  };

  // --- Data Persistence ---
  const saveTransactions = () =>
    localStorage.setItem('transactions', JSON.stringify(transactions));

  // --- Event Handlers ---
  const handleFormSubmit = (e) => {
    e.preventDefault();
    const id = document.getElementById('transactionId').value;
    const newTransaction = {
      title: document.getElementById('title').value,
      amount: parseFloat(document.getElementById('amount').value),
      type: document.querySelector('input[name="type"]:checked').value,
      category: document.getElementById('category').value,
      date: document.getElementById('date').value,
    };

    if (id) {
      const index = transactions.findIndex((t) => t.id == id);
      transactions[index] = { ...transactions[index], ...newTransaction };
    } else {
      newTransaction.id = Date.now();
      transactions.push(newTransaction);
    }
    saveTransactions();
    render();
    closeModal(transactionModal);
  };

  const handleListClick = (e) => {
    const editBtn = e.target.closest('.edit-btn');
    const deleteBtn = e.target.closest('.delete-btn');
    if (editBtn) {
      const row = editBtn.closest('tr');
      const transaction = transactions.find((t) => t.id == row.dataset.id);
      openTransactionModal(transaction);
    }
    if (deleteBtn) {
      const row = deleteBtn.closest('tr');
      const id = row.dataset.id;
      openConfirmModal({
        title: 'Delete Transaction',
        text: 'Are you sure you want to delete this transaction? This action cannot be undone.',
        onConfirm: () => {
          row.classList.add('row-deleting');
          setTimeout(() => {
            transactions = transactions.filter((t) => t.id != id);
            saveTransactions();
            render();
          }, 300);
        },
      });
    }
  };

  const handleClearAll = () => {
    openConfirmModal({
      title: 'Clear All Data',
      text: 'Are you sure you want to delete ALL transactions? This is permanent.',
      onConfirm: () => {
        transactions = [];
        saveTransactions();
        render();
      },
    });
  };

  // --- Initial Setup & Event Listeners ---
  const init = () => {
    if (localStorage.getItem('transactions') === null) {
      const today = new Date();
      const yesterday = new Date(today);
      yesterday.setDate(yesterday.getDate() - 1);
      const twoDaysAgo = new Date(today);
      twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);

      transactions = [
        {
          id: 1,
          title: 'Monthly Salary',
          amount: 3500,
          type: 'income',
          category: 'Salary',
          date: today.toISOString().split('T')[0],
        },
        {
          id: 2,
          title: 'Groceries',
          amount: 150.75,
          type: 'expense',
          category: 'Food',
          date: today.toISOString().split('T')[0],
        },
        {
          id: 3,
          title: 'Internet Bill',
          amount: 60,
          type: 'expense',
          category: 'Bills',
          date: yesterday.toISOString().split('T')[0],
        },
        {
          id: 4,
          title: 'New T-shirt',
          amount: 25.5,
          type: 'expense',
          category: 'Shopping',
          date: twoDaysAgo.toISOString().split('T')[0],
        },
      ];
      saveTransactions();
    }

    openModalBtn.addEventListener('click', () => openTransactionModal());
    closeModalBtn.addEventListener('click', () => closeModal(transactionModal));
    transactionModal.addEventListener('click', (e) => {
      if (e.target === transactionModal) closeModal(transactionModal);
    });

    cancelConfirmBtn.addEventListener('click', () => closeModal(confirmModal));
    confirmModal.addEventListener('click', (e) => {
      if (e.target === confirmModal) closeModal(confirmModal);
    });

    transactionForm.addEventListener('submit', handleFormSubmit);
    transactionListEl.addEventListener('click', handleListClick);
    searchInput.addEventListener('input', renderTransactionList);
    sortSelect.addEventListener('change', renderTransactionList);
    clearAllBtn.addEventListener('click', handleClearAll);

    render();
  };

  init();
});

Final Output:

create-expense-tracker-app-with-html-css-and-javascript.gif

Conclusion:

You’ve now built a fully working Expense Tracker App using HTML, Tailwind CSS, and JavaScript. This project helps you:

  • Understand DOM manipulation
  • Work with local storage
  • Build real-world interactive apps

This is a perfect project for your portfolio or personal use. You can even improve it by:

  • Adding a login with Firebase
  • Exporting data as CSV
  • Using a backend like Node.js later

Start tracking your spending today by building your own budget app from scratch!

That’s a wrap!

I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺