In this tutorial we’re going to learn how to create a cool todo app using html css and javascript with the dark light mode functionality
🔗Prerequisites
🔗Get started ✌
The todo app we’ll build in this tutorial will be pretty cool. a user can add or remove todo by filling todo form and delete todo by clicking delete icon , and mark todo complete by clicking circle
I’ll explain how to build each feature, but you must follow along by typing the code and running it on your end to get the most out of this tutorial
Click here to get starter files
🔗First let’s create the skeleton of the ui
Add font awesome cdn 👇
We’ll use font awesome icons in this project for that we have to include font awesome cdn in body tag you can create your own font awesome kit
js<scriptsrc="https://kit.fontawesome.com/dd8c49730d.js"crossorigin="anonymous"></script>
Markup of todo list , default todo , add todo form 👇
html<div class="container"><ul class="todo-list js-todo-list"><li class="todo-item" data-key="123456"><input id="123456" type="checkbox" /><label for="123456" class="tick js-tick"></label><span>Workout</span><button class="delete-todo js-delete-todo"><button class="delete-todo js-delete-todo"><svg fill="var(--svgcolor)" xmlns="http://www.w3.org/2000/svg" width="24" height="24"viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" /></svg></button></li></ul><form class="formselect"><input autofocus type="text" class="inputselect" placeholder="Eg: Workout"></form></div>
output of above code
🔗Let’s add styling into ui
Basic styling
csshtml {box-sizing: border-box;}*,*::before,*::after {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;line-height: 1.4;background-color: #fff;transition: all 0.5s;}
Adding styling into container, svg button , todo list , todo item, todo item span , checked button
css.container {width: 100%;max-width: 700px;margin: 0 auto;padding-left: 10px;padding-right: 10px;color: #333;margin: auto;margin-top: 10%;}.todo-list {list-style: none;margin-bottom: 20px;}.todo-item {margin-bottom: 10px;width: 100%;display: flex;align-items: center;justify-content: space-between;}.todo-item span {flex-grow: 1;margin-left: 10px;margin-right: 10px;font-size: 22px;}input[type="checkbox"] {display: none;}.tick {width: 30px;height: 30px;border: 3px solid #333;border-radius: 50%;display: inline-flex;justify-content: center;align-items: center;cursor: pointer;}
Adding styling into delete todo button
cssdelete-todo {border: none;background-color: transparent;outline: none;cursor: pointer;}.delete-todo svg {width: 30px;height: 30px;pointer-events: none;}
Adding styling into add todo form
cssform {width: 100%;display: flex;justify-content: space-between;margin-bottom: 2rem;}input[type="text"] {width: 100%;padding: 10px;border-radius: 4px;border: 3px solid #333;}input:focus {outline: none;background-color: #d4e7d5;}
Styling for todo complete effect checked ( we gonna use this in javascript )
css.done span {text-decoration: line-through;user-select: none;}.tick::before {content: "✔️";font-size: 20px;display: none;}.done .tick::before {display: inline;}
Styling done 🎨
See the complete code at the end of this step
🔗Let’s start building feature to add new todo and render todo in the list , store todo as a array object to localstorage
Function for adding new todo
jsfunction addTodo(todoText, id) {const todoobject = {todoText: document.querySelector(".inputselect").value,checked: false;id: Date.now(),};console.log(todoobject);}
In above code we create a function for adding a new todo
- We created todoobject to store all todo detail as an object property
- In todoobject we getting a value of entered input values with their id by using .value method , creating checked property and giving boolean value false and creating a id in todoobject using date.now method
- Console logged for demo purpose
Add a event listener submit to the form
jsconst form = document.querySelector(".formselect");form.addEventListener("submit", (event) => {event.preventDefault();addTodo();form.reset();});
In above code we added a event listener Submit to the form when form is submitted
- We will call our addTodo() function for adding todo
- We using reset() method to reset form after submitting a todo
See the output in console log 👇
As you can see above video after submitting a form by pressing enter we getting a new todo as an object in the console log
But we want to render a todo in todo-list to display in page to achieve that we have to create a renderTodo() function above addTodo() function
Function for rendering new todo in display
- In this function we’re going to pass todo as a parameter to access todo details from todoobject eg: todo.todoText , todo.id , todo.checked
- We’re going to select list where we will appending a all new todo items
- Select the current todo item in the DOM Check if checked is true add done class effect otherwise as it is
- Creating new element list (we’re going to use markup of todo what we created at beginning for demo purpose ) and append node in list
jsfunction renderTodo(todo) {}
Select todo list for appending all todos in list
jsfunction renderTodo(todo) {const list = document.querySelector(".todo-list");}
Select the current todo item in the DOM ( We’ll use item for deleting todo from todo list later in this tutorial )
jsfunction renderTodo(todo) {const list = document.querySelector(".todo-list");const item = document.querySelector(`[data-key='${todo.id}']`);}
Check if checked is true add done class effect otherwise as it is
jsfunction renderTodo(todo) {const list = document.querySelector(".todo-list");const item = document.querySelector(`[data-key='${todo.id}']`);const isChecked = todo.checked ? "done" : "";}
Create new element li for todo ui
- Set the class and data-key attribute
- Add html markup of todo
jsfunction renderTodo(todo) {const list = document.querySelector(".todo-list");const item = document.querySelector(`[data-key='${todo.id}']`);const isChecked = todo.checked ? "done" : "";const newlist = document.createElement("li");newlist.setAttribute("class", `todo-item ${isChecked}`);newlist.setAttribute("data-key", todo.id);}
We have to just cut and copy markup of todo ui to set the newList.innerHTML what we create at beginning for demo purpose
jsfunction renderTodo(todo) {const list = document.querySelector(".todo-list");const item = document.querySelector(`[data-key='${todo.id}']`);const isChecked = todo.checked ? "done" : "";const newlist = document.createElement("li");newlist.setAttribute("class", `todo-item ${isChecked}`);newlist.setAttribute("data-key", todo.id);newlist.innerHTML = `<input id="${todo.id}" type="checkbox"/><label for "${todo.id}" class="tick js-tick"></label><span>${todo.todoText}</span><button class="delete-todo js-delete-todo"><button class="delete-todo js-delete-todo"><svg fill="var(--svgcolor)" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" /></svg></button>`;// appending a newList in listlist.append(newlist);}
Yes here we successfully created our function for rendering a new todo in todo list But as you can see we passed todo as a parameter of function to access the property of todoobject for displaying in todo ui
To archive that we have to call renderTodo() function in addTodo function and pass todoobject object as an argument in renderTodo function
Updated addTodo as follows
jsfunction addTodo(todoText, id) {const todoobject = {todoText: document.querySelector(".inputselect").value,checked: false,id: Date.now(),};console.log(todoobject); // console log for demo purposerenderTodo(todoobject);}
In above code we called renderTodo function and passed todoobject as an argument now we can use property of todoobject in renderTodo() function
Now we can remove a default todo from list what we created at beginning for demo purpose update markup of todo list as follows
html<div class="container"><ul class="todo-list js-todo-list"><!-- default todo removed --><!-- default todo removed --></ul><form class="formselect"><inputautofocustype="text"class="inputselect"placeholder="Eg: Workout"/></form></div>
As you can see above it’s work as expected
But here we have one problem when we refresh the page all todos cleared
See below video
To keep it display we have to store data in localstorage for that we can use javascript localStorage object
Save todo data in browser local storage
- Create empty todo array for storing all todos as a array object in localstorage
- To store todo in localstorage we have to set todo array to localstorage
- Push new todoobject to todoArray after calling addTodo()
- Get todo array from localstorage to display todos in todo list
Creating empty todoArray array object
jslet todoArray = [];
Set todoArray to localstorage
We can set item to localstorage using localStorage.setItem method in renderTodo()
Add below code line in above renderTodo function
jslocalStorage.setItem("todoArray", JSON.stringify(todoArray));
Push new todoobject to todoArray array object
We have to push every new todoobject to todoArray array object after adding todo
Add below code line within addTodo
jstodoArray.push(todoobject);
Get todoArray from localstorage to display todos in todo list
To display todo in todo list we have to get the todo from todoArray after refreshing page to archive that we have to add event listener DOMContentLoaded and get the todoArray from localStorage
jsdocument.addEventListener("DOMContentLoaded", () => {const ref = localStorage.getItem("todoArray");if (ref) {todoArray = JSON.parse(ref);todoArray.forEach((t) => {renderTodo(t);});}});
Now if we refresh page it’s keep the todos in todo list
See below video
See the complete code at the end of this step
Let’s build functionality for marking todo done and delete todo
Do you remember we created id property in todoobject and set data-key attribute to item in renderTodo function now we’re going to use these property and item to Mark todo complete and delete todo from todo list
Function for marking todo complete ( in toggleDone function we’re going to )
- Find the corresponding todoobject in the todoArray object
- Set checked property
- Call renderTodo function with todoArray index
jsfunction toggleDone(key) {const index = todoArray.findIndex((myitem) => myitem.id === Number(key));todoArray[index].checked = !todoArray[index].checked;renderTodo(todoArray[index]);console.log(todoArray[index]); // for demo purposeconsole.log(index); // for demo purpose}
In above code we created function toggleDone with key parameter , to mark done on specific todo from the todo list we have to know which todo we wanted to mark done for that we find the index of corresponding todoobject from the todoArray object using findIndex method and set its checked property to the opposite. That means, true will become false and vice versa then called renderTodo()
Call toggleDone function
jsconst list = document.querySelector(".js-todo-list");list.addEventListener("click", (event) => {if (event.target.classList.contains("js-tick")) {const itemKey = event.target.parentElement.dataset.key;toggleDone(itemKey);}});
In above code we added click event listener to todo list if targeted event class list contains js-tick then get the dataset.key of their parent Element and assign to itemkey and call toggleDone() function and passed itemKey as an argument
Let’s run above code and see what’s happening
As you can see here we have problem when we mark todo done it will duplicate the todo items instead of marking todo done
To solve this we have to check if the current todo item exists in the DOM first then and replace it with the updated newlist item
Update renderTodo() function as shown below :
jsfunction renderTodo(todo) {localStorage.setItem("todoArray", JSON.stringify(todoArray));const list = document.querySelector(".todo-list");const item = document.querySelector(`[data-key='${todo.id}']`);const isChecked = todo.checked ? "done" : "";const newlist = document.createElement("li");newlist.setAttribute("class", `todo-item ${isChecked}`);newlist.setAttribute("data-key", todo.id);newlist.innerHTML = `<input id="${todo.id}" type="checkbox"/><label for "${todo.id}" class="tick js-tick"></label><span>${todo.todoText}</span><button class="delete-todo js-delete-todo"><button class="delete-todo js-delete-todo"><svg fill="var(--svgcolor)" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" /></svg></button>`;// If the item already exists in the DOMif (item) {// replace itlist.replaceChild(newlist, item);} else {// otherwise append it to the end of the listlist.append(newlist);}}
Now let’s see how it’s works
As you can see above video it’s works as expected
See the complete code at the end of this step
Function for delete todo ( in deleteTodo function we’re going to )
- Find the corresponding todoobject from the todoArray object
- Create a new object with properties of the current todoobject item and a deleted property which is set to true
- Remove the todoobject item from the array
- Render emptytodo
jsfunction deleteTodo(key) {const index = todoArray.findIndex((myitem) => myitem.id === Number(key));const emptytodo = {deleted: true,...todoArray[index],};todoArray = todoArray.filter((myitem) => myitem.id !== Number(c));renderTodo(emptytodo);}
In above code we created function deleteTodo with key parameter , to delete specific todo from the todo list we have to know which todo we wanted to delete for that we find the index of corresponding todoobject from the todoArray object using findIndex method
Created a new object with properties of the current todoobject item by using spread operatror and array[index] and a deleted property which is set to true
Filter outed deleted todo object from todoArray object using filter method
When deleteTodo() called it will remove the todoobject item from the array otherwise it will not delete todo item by click on delete button from localstorage
Call deleteTodo function ( Similarly how we called toggleDone() )
jsconst list = document.querySelector(".js-todo-list");list.addEventListener("click", (event) => {if (event.target.classList.contains("js-tick")) {const itemKey = event.target.parentElement.dataset.key;toggleDone(itemKey);}// delete todoif (event.target.classList.contains("js-delete-todo")) {const itemKey = event.target.parentElement.dataset.key;deleteTodo(itemKey);}});
Finally let’s add remove method if emptytodo delete property true it will remove todo
Add below code in renderTodo function
jsif (todo.deleted) {item.remove();return;}
Now as you can see below video we’re able to delete todo from todo list
Here we almost completed todo app project
Let’s add empty state when there is no todo to display ( optional )
Something like that
Markup of empty state ( add this markup below todo list markup in html )
html<div class="empty-state"><h1 class="app-title">My Todo</h1><i class="fas fa-tasks icon"></i><p class="empty-state__description">Type in the below field 👇 and press enter to add your todo in the list</p></div>
when there is no todo in todo list then add .empty-state and make display to flex ( add this below the other styles )
css.todo-list:empty + .empty-state {display: flex;}
Styling for empty state .empty-state by default is none
( add this below the other styles )
css.empty-state {flex-direction: column;align-items: center;justify-content: center;display: none;}.app-title {text-align: center;margin-bottom: 20px;font-size: 80px;}.checklist-icon {margin-bottom: 20px;}.icon {font-size: 7rem;}.empty-state__title,.empty-state__description {font-size: 25px;text-align: center;margin: 2rem;width: 25rem;}
Congratulation we have successfully created todo app 🥳