Introduction
A to-do app is one of the most common beginner projects—and for good reason. It’s simple, practical, and teaches you the core building blocks of web development.
In this tutorial, you’ll learn how to build a fully functional to-do list app without using any frameworks or libraries. That means no React, no Bootstrap, no jQuery—just HTML, CSS, and JavaScript.
Why Build a To-Do App?
Because it covers all the essentials a new developer should practice:
- 📄 HTML: You’ll create the structure for your input field, button, and list of tasks.
- 🎨 CSS: You’ll style the layout, highlight completed tasks, and make it user-friendly.
- 🧠 JavaScript: You’ll add interactivity like:
- Adding new tasks
- Marking tasks as complete
- Deleting tasks
- Saving tasks using
localStorage
By the end of this tutorial, you’ll have a mini productivity app that works in your browser—and the confidence to build more complex projects.
1. Project Overview
What We’ll Build
In this project, we’ll build a simple yet functional to-do list app using just HTML, CSS, and JavaScript. It will run entirely in the browser—no backend, no frameworks, no installations.
Here’s what the app will be able to do:
- ✅ Add a new task
Users can type a task into an input field and click a button to add it to the list. - 🗑️ Delete a task
Each task will have a delete button to remove it from the list. - ✔️ Mark tasks as completed
Users can click on a task to cross it off or highlight it as done. - 💾 Save tasks in the browser
Tasks will be stored usinglocalStorage
, so they won’t disappear when the page is refreshed.
By the end of this project, you’ll have an app that looks like this:
[ Enter your task here ] [Add]
- [ ] Learn JavaScript ❌
- [x] Make to-do app ❌
Tools & Technologies
You don’t need anything fancy to get started—just a few basic tools:
- HTML5 – for creating the structure of the app
- CSS3 – to style the layout and make it user-friendly
- JavaScript (ES6+) – to add interactivity and logic
- Code Editor – like VS Code or any editor of your choice
- Web Browser – such as Chrome or Firefox to test and run your app
That’s it! No installs, no dependencies—just open your editor and browser, and you’re ready to build.
2. Setting Up the Project
Before we start coding, let’s organize our project files. Keeping things tidy from the beginning makes development smoother and easier to manage.
🗂️ Folder Structure
Here’s the basic structure for our to-do app:
/todo-app
├── index.html # The main HTML file
├── style.css # All the styles for the app
└── script.js # The JavaScript logic
You can create this folder anywhere on your computer. Just make sure all three files are in the same directory.
🧾 Write the HTML code
Next, let’s set up the starting HTML file. This will be the foundation of our app.
Here’s a simple HTML5 boilerplate with links to the CSS and JavaScript files:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple To-Do App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- We’ll add the to-do app UI here later -->
<script src="script.js"></script>
</body>
</html>
What this does:
<!DOCTYPE html>
tells the browser we’re using HTML5.<meta charset="UTF-8">
ensures correct character encoding.<meta name="viewport">
makes the app responsive on mobile.<link rel="stylesheet">
loads our CSS file.<script src="script.js">
loads our JavaScript logic at the end of the page so it runs after the HTML is loaded.
3. Building the Structure
Now that we have our basic HTML file set up, it’s time to build the structure of the to-do app using clean, semantic HTML.
This part of the app includes:
- A heading to label the app
- An input box for typing tasks
- A button to add tasks
- An empty list where tasks will appear
🏗️ Add the To-Do App Markup
Let’s place our to-do UI inside the <body>
of the HTML file. We’ll wrap everything in a <section>
tag for better structure.
Here’s the markup:
<section class="todo-container">
<h1>My To-Do List</h1>
<div class="input-group">
<input type="text" id="task-input" placeholder="Enter a new task" />
<button id="add-task">Add</button>
</div>
<ul id="task-list">
<!-- Tasks will be added here -->
</ul>
</section>
🔍 What Each Element Does
<section class="todo-container">
: Groups all app content for styling and layout.<h1>
: Title of the app (you can change it to anything you like).<div class="input-group">
: Groups the input and button together for easier styling.<input type="text" id="task-input">
: Where the user types a task.<button id="add-task">
: Adds the task to the list when clicked.<ul id="task-list">
: An unordered list that will hold all the tasks dynamically using JavaScript.
This simple structure gives us a clean foundation to build on. Next, we’ll style it with CSS to make it look polished and user-friendly.
4. Styling with CSS
Now that the structure is ready, it’s time to make the app look clean and user-friendly with some basic CSS.
We’ll style the layout, input fields, buttons, and task list. We’ll also add a special class to style completed tasks, so users can easily see which ones are done.
💄 Basic Styling for the App
Open your style.css
file and paste in the following code:
/* Center the content on the page */
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px;
}
/* Main container */
.todo-container {
background-color: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
/* Heading */
h1 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
/* Input and Button */
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#task-input {
flex: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
#add-task {
padding: 10px 16px;
background-color: #007bff;
color: white;
font-weight: bold;
border: none;
border-radius: 4px;
cursor: pointer;
}
#add-task:hover {
background-color: #0056b3;
}
This code centers the app on the screen, gives it a clean card-like look, and styles the input and button for better usability.
🗂️ Style the Task List
Now let’s style the list of tasks:
#task-list {
list-style-type: none;
padding: 0;
}
#task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
background-color: #f0f0f0;
border-radius: 4px;
transition: background-color 0.3s;
}
#task-list li:hover {
background-color: #e0e0e0;
}
.delete-btn {
background-color: transparent;
color: red;
border: none;
font-size: 16px;
cursor: pointer;
}
This styles each task item to look like a card, with a delete button aligned to the right.
✅ Style Completed Tasks
We’ll add a class to style tasks that are marked as complete. You can toggle this class with JavaScript later.
.completed {
text-decoration: line-through;
color: gray;
opacity: 0.7;
}
When a task is marked as completed, it will appear faded out with a strikethrough effect. This gives a nice visual cue that it’s done.
With just these few styles, your app will already start to look clean and professional. Next, we’ll bring it to life with JavaScript!
5. Adding Interactivity with JavaScript
Now it’s time to bring the to-do app to life using JavaScript. This is where we make the app interactive—adding tasks, deleting them, and marking them as complete.
We’ll write a few core functions and attach event listeners to key elements like buttons and list items. Don’t worry—each part is simple and easy to follow.
Core Functions
We’ll use three main functions to handle the app’s logic:
1. addTask()
This function will:
- Get the input value
- Create a new
<li>
element - Add a delete button
- Append it to the task list
2. removeTask(e)
This function will:
- Remove the clicked task from the list
It will be triggered when the user clicks the delete (❌) button.
3. toggleComplete(e)
This function will:
- Toggle the
.completed
class on a task item
It lets users click a task to mark it as done or undo it.
Event Listeners
Let’s add event listeners to make things work:
1. Add Task Button
Attach a click event to the “Add” button to run the addTask()
function.
document.getElementById('add-task').addEventListener('click', addTask);
2. Delete Button (Dynamically Created)
For each task, we’ll create a delete button with a class like delete-btn
. Inside addTask()
, you’ll attach a click event to it:
deleteBtn.addEventListener('click', removeTask);
3. Toggle Completion
We’ll also add a click event to each task item (<li>
) that toggles the .completed
class:
li.addEventListener('click', toggleComplete);
This way, users can click anywhere on the task to mark it done—no extra checkbox needed.
Input Validation
We don’t want empty tasks showing up in the list. So before we add a task, we’ll check if the input is not blank:
function addTask() {
const input = document.getElementById('task-input');
const taskText = input.value.trim();
if (taskText === '') {
alert('Please enter a task.');
return;
}
// Proceed with creating the task item...
}
This small check improves user experience and keeps the list clean.
With these functions and listeners in place, your app will feel dynamic and interactive. Next, we’ll make sure the tasks stay even after refreshing the page using localStorage
.
6. Saving Tasks with localStorage
One important feature of any useful to-do app is saving your tasks so they don’t disappear when you close or refresh the browser. That’s where localStorage
comes in.
Why Use localStorage?
localStorage
is a simple browser API that lets you store data locally on a user’s computer.- It keeps your data even after refreshing or closing the browser — perfect for saving tasks.
- Unlike cookies,
localStorage
stores more data and doesn’t get sent with every web request. - It’s easy to use and works in all modern browsers.
Implement Save & Load Functions
We’ll create two main functions to manage saving and loading tasks:
1. saveTasks()
This function will:
- Grab all current tasks from the list
- Store their text and completion status in an array
- Convert the array to a JSON string
- Save it to
localStorage
under a key like"tasks"
Example:
function saveTasks() {
const tasks = [];
document.querySelectorAll('#task-list li').forEach(li => {
tasks.push({
text: li.firstChild.textContent,
completed: li.classList.contains('completed')
});
});
localStorage.setItem('tasks', JSON.stringify(tasks));
}
2. loadTasks()
This function will:
- Retrieve the tasks JSON string from
localStorage
- Parse it back into an array
- Loop through the array and recreate each task item in the list
- Apply the
.completed
class where needed
Example:
function loadTasks() {
const tasks = JSON.parse(localStorage.getItem('tasks')) || [];
tasks.forEach(task => {
const li = document.createElement('li');
li.textContent = task.text;
if (task.completed) {
li.classList.add('completed');
}
// Add event listeners for toggle and delete here (same as addTask)
li.addEventListener('click', toggleComplete);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '❌';
deleteBtn.className = 'delete-btn';
deleteBtn.addEventListener('click', removeTask);
li.appendChild(deleteBtn);
document.getElementById('task-list').appendChild(li);
});
}
To understand how
localStorage
works and why it’s useful for saving tasks in your browser, see the MDN localStorage guide for a clear explanation with examples.
When to Use These Functions
- Call
loadTasks()
when the page loads to restore saved tasks. - Call
saveTasks()
whenever you add, remove, or complete a task to keep the storage updated.
With this setup, your to-do list will remember your tasks every time you visit or refresh the page. Next, we’ll review the full code and how everything fits together
7. Final Code Review
Full Code Breakdown
Here’s the complete code for your simple to-do app, combining everything we built so far.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple To-Do App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<section class="todo-container">
<h1>My To-Do List</h1>
<div class="input-group">
<input type="text" id="task-input" placeholder="Enter a new task" />
<button id="add-task">Add</button>
</div>
<ul id="task-list"></ul>
</section>
<script src="script.js"></script>
</body>
</html>
style.css
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px;
}
.todo-container {
background-color: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
h1 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#task-input {
flex: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
#add-task {
padding: 10px 16px;
background-color: #007bff;
color: white;
font-weight: bold;
border: none;
border-radius: 4px;
cursor: pointer;
}
#add-task:hover {
background-color: #0056b3;
}
#task-list {
list-style-type: none;
padding: 0;
}
#task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
background-color: #f0f0f0;
border-radius: 4px;
transition: background-color 0.3s;
}
#task-list li:hover {
background-color: #e0e0e0;
}
.delete-btn {
background-color: transparent;
color: red;
border: none;
font-size: 16px;
cursor: pointer;
}
.completed {
text-decoration: line-through;
color: gray;
opacity: 0.7;
}
script.js
// Add event listener to "Add" button
document.getElementById('add-task').addEventListener('click', addTask);
// Load tasks from localStorage when page loads
window.addEventListener('DOMContentLoaded', loadTasks);
function addTask() {
const input = document.getElementById('task-input');
const taskText = input.value.trim();
if (taskText === '') {
alert('Please enter a task.');
return;
}
const li = document.createElement('li');
li.textContent = taskText;
// Toggle completion on click
li.addEventListener('click', toggleComplete);
// Create delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '❌';
deleteBtn.className = 'delete-btn';
deleteBtn.addEventListener('click', removeTask);
li.appendChild(deleteBtn);
document.getElementById('task-list').appendChild(li);
input.value = '';
saveTasks();
}
function removeTask(e) {
e.stopPropagation(); // Prevent toggleComplete from triggering
const li = e.target.parentElement;
li.remove();
saveTasks();
}
function toggleComplete(e) {
e.target.classList.toggle('completed');
saveTasks();
}
function saveTasks() {
const tasks = [];
document.querySelectorAll('#task-list li').forEach(li => {
tasks.push({
text: li.firstChild.textContent,
completed: li.classList.contains('completed')
});
});
localStorage.setItem('tasks', JSON.stringify(tasks));
}
function loadTasks() {
const tasks = JSON.parse(localStorage.getItem('tasks')) || [];
tasks.forEach(task => {
const li = document.createElement('li');
li.textContent = task.text;
if (task.completed) {
li.classList.add('completed');
}
li.addEventListener('click', toggleComplete);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '❌';
deleteBtn.className = 'delete-btn';
deleteBtn.addEventListener('click', removeTask);
li.appendChild(deleteBtn);
document.getElementById('task-list').appendChild(li);
});
}
🧪 How It Works
- HTML defines the structure: a heading, input field, add button, and an empty list for tasks.
- CSS styles the app: centers content, styles inputs/buttons, and visually distinguishes completed tasks.
- JavaScript:
- Listens for user input and button clicks.
- Creates and removes tasks dynamically.
- Toggles task completion styles.
- Saves and loads tasks using
localStorage
to keep data persistent between sessions.
This simple but complete app covers important web development concepts and is a great starting point for building more complex projects.
If you want to see or edit this live, here’s my code in CodePen, you can fork and experiment with it.
Conclusion
Congratulations! You’ve just built your very own simple to-do app using only HTML, CSS, and JavaScript — no frameworks needed. 🎉
By working through this project, you learned how to:
- Structure a web page with semantic HTML
- Style elements with CSS to create a clean user interface
- Use JavaScript to add interactivity like adding, deleting, and completing tasks
- Store data locally with
localStorage
so your tasks persist across sessions
Building projects like this is a great way to practice and strengthen your web development skills.
What’s Next?
- Try customizing your app by adding new features like due dates, task categories, or a “clear all” button.
- Experiment with different styles to make the design your own.
- Explore other beginner-friendly projects on this blog under the Build Projects category.
If you found this tutorial helpful, please leave a comment below sharing your experience or any questions you have. Don’t forget to subscribe for more easy-to-follow coding tutorials that help you stay ahead in cloud, code, and career growth!
Happy coding! 🚀🚀