Press ESC to close

How to Build a Simple To-Do List Application with HTML, CSS, and JavaScript

In this comprehensive guide, we’ll walk through creating a feature-rich to-do list application using core web technologies: HTML for structure, CSS for styling, and JavaScript for functionality. This project will help you understand fundamental web development concepts while building a practical application. By the end, you’ll have a working to-do list with task management, persistence, and interactive features.

Table of Contents

  1. Introduction to To-Do List Applications
  2. Project Features Overview
  3. Setting Up the HTML Structure
  4. Styling with CSS
  5. Core JavaScript Functionality
  6. Implementing Local Storage
  7. Enhancing the Application
  8. Conclusion and Next Steps

1. Introduction to To-Do List Applications

To-do list applications are excellent beginner projects that teach essential web development skills:

  • DOM manipulation
  • Event handling
  • Data persistence
  • CRUD operations (Create, Read, Update, Delete)
  • Responsive design

Our application will include:

  • Adding new tasks
  • Marking tasks as complete
  • Deleting tasks
  • Local storage for data persistence
  • Edit functionality
  • Task filtering
  • Smooth animations

2. Project Features Overview

Before diving into the code, let’s outline our application’s features:

  1. Task Management
  • Add new tasks
  • Edit existing tasks
  • Delete tasks
  • Toggle completion status
  1. Data Persistence
  • Save tasks to localStorage
  • Load tasks on page refresh
  1. User Interface
  • Clean, responsive design
  • Visual feedback for actions
  • Filtering options (All/Active/Completed)
  • Animations for task interactions

3. Setting Up the HTML Structure

Create a new index.html file with this base structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo List App</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Todo List</h1>
        <div class="input-group">
            <input type="text" id="taskInput" placeholder="Enter new task...">
            <button id="addTaskBtn">Add Task</button>
        </div>
        <div class="filter-buttons">
            <button class="filter-btn active" data-filter="all">All</button>
            <button class="filter-btn" data-filter="active">Active</button>
            <button class="filter-btn" data-filter="completed">Completed</button>
        </div>
        <ul id="taskList"></ul>
    </div>
    <script src="app.js"></script>
</body>
</html>

HTML Breakdown:

  • Container for organization
  • Input field and button for new tasks
  • Filter buttons to view different task states
  • Unordered list to display tasks
  • External CSS and JavaScript files

4. Styling with CSS

Create styles.css with these styles:

:root {
    --primary-color: #4a90e2;
    --secondary-color: #f44336;
    --background-color: #f5f5f5;
    --text-color: #333;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Arial', sans-serif;
    background-color: var(--background-color);
    color: var(--text-color);
    line-height: 1.6;
}

.container {
    max-width: 800px;
    margin: 2rem auto;
    padding: 1.5rem;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

h1 {
    text-align: center;
    margin-bottom: 1.5rem;
    color: var(--primary-color);
}

.input-group {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
}

#taskInput {
    flex: 1;
    padding: 0.8rem;
    border: 2px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}

#addTaskBtn {
    padding: 0.8rem 1.5rem;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.3s;
}

#addTaskBtn:hover {
    background-color: #357abd;
}

.filter-buttons {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.filter-btn {
    flex: 1;
    padding: 0.5rem;
    background-color: #eee;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s;
}

.filter-btn.active {
    background-color: var(--primary-color);
    color: white;
}

#taskList {
    list-style: none;
}

.task-item {
    display: flex;
    align-items: center;
    padding: 0.8rem;
    margin-bottom: 0.5rem;
    background-color: #f8f8f8;
    border-radius: 4px;
    transition: all 0.3s;
}

.task-item:hover {
    transform: translateX(5px);
    box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}

.task-item.completed {
    background-color: #e8e8e8;
    opacity: 0.7;
}

.task-item.completed .task-text {
    text-decoration: line-through;
}

.task-text {
    flex: 1;
    margin: 0 1rem;
}

.task-actions {
    display: flex;
    gap: 0.5rem;
}

.edit-btn, .delete-btn {
    padding: 0.4rem 0.8rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: opacity 0.3s;
}

.edit-btn {
    background-color: #ffc107;
    color: white;
}

.delete-btn {
    background-color: var(--secondary-color);
    color: white;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(-10px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes fadeOut {
    from { opacity: 1; transform: translateX(0); }
    to { opacity: 0; transform: translateX(100px); }
}

.fade-in {
    animation: fadeIn 0.3s ease-out;
}

.fade-out {
    animation: fadeOut 0.3s ease-in forwards;
}

CSS Highlights:

  • Modern box-shadow and transitions for depth
  • Responsive layout with flexbox
  • State-based styling (completed tasks)
  • Hover effects for better UX
  • Animations for task additions/removals
  • CSS variables for easy theming

5. Core JavaScript Functionality

Create app.js with this initial code:

class TodoList {
    constructor() {
        this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];
        this.taskInput = document.getElementById('taskInput');
        this.addTaskBtn = document.getElementById('addTaskBtn');
        this.taskList = document.getElementById('taskList');
        this.filterButtons = document.querySelectorAll('.filter-btn');
        this.currentFilter = 'all';

        this.initializeEventListeners();
        this.renderTasks();
    }

    initializeEventListeners() {
        this.addTaskBtn.addEventListener('click', () => this.addTask());
        this.taskInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') this.addTask();
        });

        this.taskList.addEventListener('click', (e) => {
            if (e.target.classList.contains('delete-btn')) {
                this.deleteTask(e.target.closest('.task-item').dataset.id);
            }
            if (e.target.classList.contains('edit-btn')) {
                this.editTask(e.target.closest('.task-item').dataset.id);
            }
            if (e.target.type === 'checkbox') {
                this.toggleTask(e.target.closest('.task-item').dataset.id);
            }
        });

        this.filterButtons.forEach(button => {
            button.addEventListener('click', () => this.filterTasks(button.dataset.filter));
        });
    }

    addTask() {
        const taskText = this.taskInput.value.trim();
        if (!taskText) return;

        const newTask = {
            id: Date.now(),
            text: taskText,
            completed: false
        };

        this.tasks.push(newTask);
        this.saveTasks();
        this.renderTasks();
        this.taskInput.value = '';
    }

    deleteTask(taskId) {
        this.tasks = this.tasks.filter(task => task.id !== Number(taskId));
        this.saveTasks();
        this.renderTasks();
    }

    toggleTask(taskId) {
        this.tasks = this.tasks.map(task => 
            task.id === Number(taskId) ? {...task, completed: !task.completed} : task
        );
        this.saveTasks();
        this.renderTasks();
    }

    editTask(taskId) {
        const task = this.tasks.find(task => task.id === Number(taskId));
        const newText = prompt('Edit task:', task.text);
        if (newText !== null) {
            task.text = newText.trim();
            this.saveTasks();
            this.renderTasks();
        }
    }

    filterTasks(filter) {
        this.currentFilter = filter;
        this.filterButtons.forEach(btn => 
            btn.classList.toggle('active', btn.dataset.filter === filter)
        );
        this.renderTasks();
    }

    saveTasks() {
        localStorage.setItem('tasks', JSON.stringify(this.tasks));
    }

    renderTasks() {
        this.taskList.innerHTML = '';
        const filteredTasks = this.tasks.filter(task => {
            if (this.currentFilter === 'active') return !task.completed;
            if (this.currentFilter === 'completed') return task.completed;
            return true;
        });

        filteredTasks.forEach(task => {
            const taskElement = document.createElement('li');
            taskElement.className = `task-item fade-in ${task.completed ? 'completed' : ''}`;
            taskElement.dataset.id = task.id;
            taskElement.innerHTML = `
                <input type="checkbox" ${task.completed ? 'checked' : ''}>
                <span class="task-text">${task.text}</span>
                <div class="task-actions">
                    <button class="edit-btn">Edit</button>
                    <button class="delete-btn">Delete</button>
                </div>
            `;
            this.taskList.appendChild(taskElement);
        });
    }
}

// Initialize application
const todoApp = new TodoList();

JavaScript Breakdown:

  • TodoList class encapsulates functionality
  • Constructor initializes state and DOM references
  • Event delegation for dynamic elements
  • Array methods for task manipulation
  • LocalStorage integration
  • Filtering logic
  • ES6 features like arrow functions and template literals

6. Implementing Local Storage

Our application uses the browser’s localStorage to persist data:

// In constructor
this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];

// Save method
saveTasks() {
    localStorage.setItem('tasks', JSON.stringify(this.tasks));
}

Key Points:

  • Data persists even after browser restart
  • Maximum storage limit of ~5MB
  • Data stored as strings (requires JSON conversion)
  • Synchronous operation (consider IndexedDB for larger datasets)

7. Enhancing the Application

Let’s add more features to improve functionality:

7.1 Task Editing with Inline Input

Modify the editTask method:

editTask(taskId) {
    const taskItem = document.querySelector(`[data-id="${taskId}"]`);
    const taskText = taskItem.querySelector('.task-text');
    const currentText = taskText.textContent;

    const input = document.createElement('input');
    input.type = 'text';
    input.value = currentText;
    input.className = 'edit-input';

    taskText.replaceWith(input);
    input.focus();

    const saveEdit = () => {
        const newText = input.value.trim();
        if (newText) {
            const task = this.tasks.find(t => t.id === Number(taskId));
            task.text = newText;
            this.saveTasks();
            this.renderTasks();
        }
    };

    input.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') saveEdit();
    });

    input.addEventListener('blur', saveEdit);
}

7.2 Improved Animations

Add these CSS transitions:

.task-item {
    transition: all 0.3s ease-out;
}

.task-item.fade-out {
    opacity: 0;
    transform: translateX(100%);
}

Update the delete method:

deleteTask(taskId) {
    const taskItem = document.querySelector(`[data-id="${taskId}"]`);
    taskItem.classList.add('fade-out');

    setTimeout(() => {
        this.tasks = this.tasks.filter(task => task.id !== Number(taskId));
        this.saveTasks();
        this.renderTasks();
    }, 300);
}

7.3 Character Counter

Add to HTML:

<div class="char-counter">
    <span id="charCount">0</span>/140
</div>

Add to JavaScript:

updateCharCounter() {
    const count = this.taskInput.value.length;
    document.getElementById('charCount').textContent = count;
    if (count > 140) {
        this.taskInput.classList.add('error');
    } else {
        this.taskInput.classList.remove('error');
    }
}

// Add event listener
this.taskInput.addEventListener('input', () => this.updateCharCounter());

8. Conclusion and Next Steps

You’ve now built a full-featured to-do list application with:

  • Task management
  • Local storage
  • Filtering
  • Animations
  • Edit functionality

Next Enhancement Ideas:

  1. Due dates and priorities
  2. Drag-and-drop reordering
  3. Categories/tags
  4. Server synchronization
  5. User authentication
  6. Dark mode toggle

This project demonstrates core web development concepts and provides a foundation for building more complex applications. Continue experimenting with features and refining the UI to deepen your understanding of front-end development.

Final Code Availability:
All code files are available on [GitHub Repository Link]

Leave a Reply

Your email address will not be published. Required fields are marked *