Build a Modern Todo App with Vanilla JavaScript: A Beginner’s Guide

In today’s fast-paced digital world, managing tasks efficiently is more crucial than ever. From simple grocery lists to complex project management, the ability to organize and track what needs to be done is a fundamental skill. And what better way to master this skill, and simultaneously level up your web development abilities, than by building your very own Todo application? This tutorial will guide you, step-by-step, through creating a modern, functional Todo app using only Vanilla JavaScript—no frameworks or libraries required. By the end, you’ll have a fully operational application and a solid understanding of core JavaScript concepts. Let’s dive in!

Why Build a Todo App?

Creating a Todo app is more than just a coding exercise; it’s a fantastic learning opportunity. It allows you to:

  • Practice fundamental JavaScript concepts: You’ll work with variables, data types, functions, DOM manipulation, event listeners, and local storage.
  • Understand the user interface (UI) and user experience (UX): You’ll learn how to structure your app for easy use and intuitive navigation.
  • Enhance your problem-solving skills: You’ll break down a complex problem (task management) into smaller, manageable parts.
  • Boost your portfolio: A functional Todo app is a great project to showcase your skills to potential employers.

Plus, it’s a practical project that you can use daily to manage your own tasks!

Project Setup and HTML Structure

Before we start coding, let’s set up our project. Create a new folder for your Todo app. Inside that folder, create three files:

  • index.html: This file will contain the HTML structure of your app.
  • style.css: This file will hold the CSS styles for your app.
  • script.js: This file will contain the JavaScript code for the app’s functionality.

Open index.html in your code editor and add the following HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Todo App</h1>
        <div class="input-container">
            <input type="text" id="todoInput" placeholder="Add a new task...">
            <button id="addButton">Add</button>
        </div>
        <ul id="todoList">
            <!-- Todo items will be added here -->
        </ul>
    </div>
    <script src="script.js"></script>
</body>
</html>

Let’s break down this HTML:

  • <head>: Contains metadata, including the title and a link to the CSS stylesheet.
  • <body>: Contains the visible content of our app.
  • <div class="container">: The main container, holding all elements.
  • <h1>: The title of our app.
  • <div class="input-container">: Contains the input field and the add button.
  • <input type="text" id="todoInput">: The input field where users will type their tasks.
  • <button id="addButton">: The button to add a new task.
  • <ul id="todoList">: The unordered list where todo items will be displayed.
  • <script src="script.js">: Links to the JavaScript file, where we’ll write the logic.

Now, let’s add some basic styling to style.css. This is a starting point; feel free to customize it to your liking:

body {
    font-family: sans-serif;
    background-color: #f4f4f4;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.container {
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 80%;
    max-width: 500px;
}

h1 {
    text-align: center;
    color: #333;
}

.input-container {
    display: flex;
    margin-bottom: 10px;
}

#todoInput {
    flex-grow: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 16px;
}

#addButton {
    padding: 10px 15px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    margin-left: 10px;
}

#addButton:hover {
    background-color: #3e8e41;
}

#todoList {
    list-style: none;
    padding: 0;
}

#todoList li {
    padding: 10px;
    border-bottom: 1px solid #eee;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

#todoList li:last-child {
    border-bottom: none;
}

.deleteButton {
    background-color: #f44336;
    color: white;
    border: none;
    padding: 5px 10px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.deleteButton:hover {
    background-color: #da190b;
}

.completed {
    text-decoration: line-through;
    color: #888;
}

This CSS provides basic styling for the layout, input field, button, and list items. Save both files, and you should see a basic, styled Todo app interface in your browser.

JavaScript: Adding Functionality

Now, let’s bring our app to life with JavaScript. Open script.js and start by selecting the HTML elements we’ll be interacting with:

// Select HTML elements
const todoInput = document.getElementById('todoInput');
const addButton = document.getElementById('addButton');
const todoList = document.getElementById('todoList');

Next, we’ll create a function to add a new todo item. This function will:

  • Get the text from the input field.
  • Create a new list item (<li>).
  • Create a delete button for the item.
  • Append the task text and the delete button to the list item.
  • Append the list item to the todoList.
  • Clear the input field.
// Function to add a new todo item
function addTodo() {
    const taskText = todoInput.value.trim();
    if (taskText !== '') {
        const listItem = document.createElement('li');
        listItem.textContent = taskText;

        // Create delete button
        const deleteButton = document.createElement('button');
        deleteButton.textContent = 'Delete';
        deleteButton.classList.add('deleteButton');
        deleteButton.addEventListener('click', deleteTodo);

        // Create complete button
        const completeButton = document.createElement('button');
        completeButton.textContent = 'Complete';
        completeButton.classList.add('completeButton');
        completeButton.addEventListener('click', completeTodo);

        listItem.appendChild(deleteButton);
        todoList.appendChild(listItem);
        todoInput.value = '';
    }
}

Let’s break down the addTodo function:

  • const taskText = todoInput.value.trim();: Gets the value from the input field and removes any leading/trailing whitespace using trim().
  • if (taskText !== '') { ... }: Checks if the input is not empty before creating a todo item.
  • const listItem = document.createElement('li');: Creates a new list item element.
  • listItem.textContent = taskText;: Sets the text content of the list item to the task text.
  • const deleteButton = document.createElement('button');: Creates a delete button.
  • deleteButton.textContent = 'Delete';: Sets the text content of the delete button.
  • deleteButton.classList.add('deleteButton');: Adds a CSS class to the delete button for styling.
  • deleteButton.addEventListener('click', deleteTodo);: Adds an event listener to the delete button, calling the deleteTodo function when clicked. We’ll define this later.
  • listItem.appendChild(deleteButton);: Appends the delete button to the list item.
  • todoList.appendChild(listItem);: Appends the list item to the todo list.
  • todoInput.value = '';: Clears the input field.

Now, let’s add the functionality for deleting a todo item. This function will:

  • Remove the list item from the todoList.
// Function to delete a todo item
function deleteTodo(event) {
    const listItem = event.target.parentNode;
    todoList.removeChild(listItem);
}

Here’s how deleteTodo works:

  • const listItem = event.target.parentNode;: Gets the parent element of the clicked button (which is the <li> element).
  • todoList.removeChild(listItem);: Removes the list item from the todoList.

Next, let’s add the ability to mark a todo item as complete. This function will:

  • Toggle a CSS class (e.g., ‘completed’) on the list item.

function completeTodo(event) {
    const listItem = event.target.parentNode;
    listItem.classList.toggle('completed');
}

Here’s how completeTodo works:

  • const listItem = event.target.parentNode;: Gets the parent element of the clicked button (which is the <li> element).
  • listItem.classList.toggle('completed');: Toggles the ‘completed’ class on the list item. If the class is present, it removes it; if it’s not present, it adds it.

Finally, we need to attach the addTodo function to the “Add” button’s click event. Add the following code to the end of script.js:

// Add event listener to the add button
addButton.addEventListener('click', addTodo);

Now, your app should be functional! You can add tasks, and delete them. However, your app currently doesn’t persist the tasks if you refresh the page. Let’s fix that.

Adding Local Storage

To make our Todo app persistent (i.e., save tasks even after the page is refreshed), we’ll use local storage. Local storage allows us to store data in the user’s browser.

First, we need to save the tasks to local storage whenever a new task is added or deleted. Modify the addTodo function to save the tasks to local storage:


function addTodo() {
    const taskText = todoInput.value.trim();
    if (taskText !== '') {
        const listItem = document.createElement('li');
        listItem.textContent = taskText;

        // Create delete button
        const deleteButton = document.createElement('button');
        deleteButton.textContent = 'Delete';
        deleteButton.classList.add('deleteButton');
        deleteButton.addEventListener('click', deleteTodo);

        // Create complete button
        const completeButton = document.createElement('button');
        completeButton.textContent = 'Complete';
        completeButton.classList.add('completeButton');
        completeButton.addEventListener('click', completeTodo);

        listItem.appendChild(deleteButton);
        todoList.appendChild(listItem);
        todoInput.value = '';

        // Save to local storage
        saveTodos();
    }
}

Now, let’s modify the deleteTodo function to update local storage as well:


function deleteTodo(event) {
    const listItem = event.target.parentNode;
    todoList.removeChild(listItem);

    // Save to local storage after deleting
    saveTodos();
}

Next, let’s modify the completeTodo function to update local storage:


function completeTodo(event) {
    const listItem = event.target.parentNode;
    listItem.classList.toggle('completed');

    // Save to local storage after completing
    saveTodos();
}

We’ve added calls to saveTodos() inside addTodo(), deleteTodo() and completeTodo(). Now, let’s create the saveTodos() function. This function will:

  • Get all the todo items from the todoList.
  • Create an array of task objects (task text and completion status).
  • Convert the array to a JSON string.
  • Store the JSON string in local storage under a key (e.g., ‘todos’).

function saveTodos() {
    const todos = [];
    const todoItems = todoList.children; // Get all li elements

    for (let i = 0; i < todoItems.length; i++) {
        const item = todoItems[i];
        const text = item.textContent;
        const isCompleted = item.classList.contains('completed');
        todos.push({ text, completed: isCompleted });
    }

    localStorage.setItem('todos', JSON.stringify(todos));
}

Let’s also create a function to load the todos from local storage when the page loads. This function will:

  • Check if there are any todos stored in local storage.
  • If there are, parse the JSON string back into an array of task objects.
  • Loop through the array and create list items for each task.
  • Append the list items to the todoList.

function loadTodos() {
    const storedTodos = localStorage.getItem('todos');
    if (storedTodos) {
        const todos = JSON.parse(storedTodos);
        todos.forEach(todo => {
            const listItem = document.createElement('li');
            listItem.textContent = todo.text;
            if (todo.completed) {
                listItem.classList.add('completed');
            }

            // Create delete button
            const deleteButton = document.createElement('button');
            deleteButton.textContent = 'Delete';
            deleteButton.classList.add('deleteButton');
            deleteButton.addEventListener('click', deleteTodo);

            // Create complete button
            const completeButton = document.createElement('button');
            completeButton.textContent = 'Complete';
            completeButton.classList.add('completeButton');
            completeButton.addEventListener('click', completeTodo);

            listItem.appendChild(deleteButton);
            todoList.appendChild(listItem);
        });
    }
}

Finally, call the loadTodos() function when the page loads. Add this line at the end of script.js, before the event listener for the add button:

loadTodos();

With these changes, your Todo app will now save and load tasks from local storage, making it persistent across page refreshes!

Common Mistakes and Troubleshooting

While building your Todo app, you might encounter some common issues. Here are some of them and how to fix them:

1. Tasks Not Saving to Local Storage

Problem: You add tasks, but they disappear when you refresh the page.

Solution:

  • Check your saveTodos() function: Make sure it’s correctly getting the task text and completion status from the list items and storing them in local storage. Verify the task objects are being created correctly.
  • Check your loadTodos() function: Ensure it’s correctly retrieving the tasks from local storage and adding them back to the todoList. Inspect the browser’s console for any errors during parsing.
  • Verify the data in local storage: Use your browser’s developer tools (Application -> Local Storage) to see if the ‘todos’ key exists and contains the correct data. If it’s empty, or the data is malformed, there’s a problem with how you’re saving or retrieving the data.
  • Check for typos: Double-check the spelling of the local storage key (‘todos’).

2. Delete Button Not Working

Problem: Clicking the delete button doesn’t remove the task.

Solution:

  • Check the event listener: Make sure the deleteTodo function is correctly attached to the delete button’s click event.
  • Inspect the deleteTodo function: Ensure it correctly identifies the parent list item and removes it from the todoList. Use console.log(event.target.parentNode) inside the deleteTodo function to verify you’re selecting the correct element.
  • Check for typos: Double-check the spelling of the function name in the event listener.

3. Tasks Not Being Marked as Complete

Problem: Clicking the “Complete” button doesn’t mark the task as complete (e.g., strike through the text).

Solution:

  • Check the completeTodo function: Make sure it correctly toggles the ‘completed’ class on the list item.
  • Check your CSS: Ensure you have CSS rules that style the .completed class to strike through the text (e.g., text-decoration: line-through;).
  • Inspect the class application: Use the browser’s developer tools to check if the ‘completed’ class is being added to the list item when the button is clicked.
  • Verify the data in local storage: When loading tasks, make sure to add the ‘completed’ class to the list item if its corresponding value in the local storage is true.

4. Styling Issues

Problem: The app’s appearance doesn’t match the CSS you wrote.

Solution:

  • Check the file paths: Make sure the link to your CSS file in index.html is correct (e.g., <link rel="stylesheet" href="style.css">).
  • Inspect the CSS rules: Use your browser’s developer tools (Elements -> Styles) to see if your CSS rules are being applied. Check for any conflicts or overrides.
  • Clear your cache: Sometimes, the browser might cache the old CSS. Try clearing your browser’s cache or using a hard refresh (Ctrl+Shift+R or Cmd+Shift+R).
  • Check for CSS errors: Use a CSS validator to check for any syntax errors in your CSS code.

Enhancements and Next Steps

Congratulations! You’ve successfully built a basic but functional Todo app. Here are some ideas to enhance and expand your app:

  • Add Edit Functionality: Allow users to edit existing tasks. This would involve adding an “Edit” button, and a way to update the task text.
  • Implement Filtering: Add options to filter tasks by status (e.g., “All,” “Active,” “Completed”).
  • Add a Clear Completed Button: Provide a button to remove all completed tasks.
  • Improve the UI: Add more styling, use icons, and make the app more visually appealing. Consider using a CSS framework like Bootstrap or Tailwind CSS to speed up styling.
  • Implement Drag and Drop: Allow users to reorder tasks by dragging and dropping them.
  • Add Due Dates: Allow users to set due dates for each task.
  • Implement a Backend: For more complex applications, consider integrating a backend to store the data on a server (e.g., using Node.js, Python/Django, or PHP/Laravel).

Remember, the best way to learn is by doing. Experiment with these enhancements, try new things, and don’t be afraid to make mistakes. Each error is an opportunity to learn and grow as a developer.

Key Takeaways

  • You’ve learned the fundamentals of building a web application with Vanilla JavaScript.
  • You understand how to manipulate the DOM, handle events, and use local storage.
  • You’ve gained practical experience with HTML, CSS, and JavaScript.
  • You have a working Todo app to manage your tasks.
  • You now have a solid foundation for building more complex web applications.

FAQ

Here are some frequently asked questions about building a Todo app with Vanilla JavaScript:

  1. Q: Why should I build a Todo app with Vanilla JavaScript instead of using a framework like React or Vue.js?

    A: Building a Todo app with Vanilla JavaScript helps you understand the underlying principles of web development. You learn how the browser works, how to interact with the DOM directly, and how to manage state. This knowledge is invaluable, even if you eventually use frameworks. It provides a deeper understanding of what the frameworks are doing under the hood.

  2. Q: What are the benefits of using local storage?

    A: Local storage allows you to persist data in the user’s browser, so your Todo app can save tasks even when the user closes the browser or refreshes the page. This provides a better user experience, as users don’t have to re-enter their tasks every time they visit the app.

  3. Q: How can I debug my JavaScript code if something goes wrong?

    A: Use the browser’s developer tools (usually accessed by right-clicking on the page and selecting “Inspect”). The “Console” tab is invaluable for logging messages, checking variable values, and identifying errors. You can use console.log() to print variables to the console, and you can set breakpoints in your code to step through it line by line.

  4. Q: Can I use this Todo app on my mobile phone?

    A: Yes, your Todo app should work on mobile devices, as it’s built with standard HTML, CSS, and JavaScript. However, you might need to adjust the CSS to make it responsive and ensure it looks good on different screen sizes. Consider using media queries in your CSS to adapt the layout for smaller screens.

  5. Q: What are some good resources for learning more about JavaScript?

    A: There are many excellent resources available. Some popular options include:

    • MDN Web Docs (Mozilla Developer Network): A comprehensive resource for web technologies, including JavaScript.
    • freeCodeCamp.org: Provides free coding tutorials and projects.
    • JavaScript.info: A well-structured and in-depth JavaScript tutorial.
    • Scrimba: Interactive coding tutorials with screencasts.
    • Online courses on platforms like Coursera, Udemy, and Codecademy.

Building a Todo app is a rewarding journey that combines practicality with learning. From structuring the HTML to implementing the core JavaScript logic and adding persistence with local storage, you’ve gained a comprehensive understanding of how to build a functional web application. Remember, the key to mastering web development, and any programming discipline, is consistent practice and a willingness to explore. So, keep building, keep experimenting, and keep learning. Your journey as a web developer has just begun!