1.Introduction
Every developer has that one project they keep coming back to — the classic Todo App. It sounds simple on the surface, but building one from scratch teaches you a surprising amount about DOM manipulation, state management, browser storage, and UI design. In this blog, I'll walk you through how I built my own Todo App using pure HTML, CSS, and JavaScript — no frameworks, no libraries (except a little confetti surprise). I'll talk about the features I built, the decisions I made along the way, and the improvements I plan to add in the future.
2.Why a Todo App?
Before diving in, you might be wondering — why another Todo App? The honest answer is that it's one of the best exercises for a frontend developer. A Todo App forces you to think about:
How to add, update, and remove items dynamically in the DOM
How to persist data so it survives a page refresh
How to track state and reflect it visually in real time
How to style a UI that actually feels good to use
It's the perfect project to solidify your core JavaScript skills before jumping into frameworks like React or Vue.
3.Tech Stack
I intentionally kept this project framework-free to focus on the fundamentals:
HTML5 — Semantic structure and layout
CSS3 — Custom dark theme using CSS variables, flexbox, and smooth transitions
JavaScript — All the logic: DOM manipulation, event handling, and localStorage
@hiseb/confetti — A lightweight CDN library for the celebration animation
No build tools, no npm install, no webpack. Just three files and a browser.
4.Features I Built
Adding Tasks
The core feature — a text input and a + button. When the user types a task and clicks the button (or submits the form), the task gets pushed into a tasks array as an object { text, completed: false }, and the list re-renders.
const addTask = () => {
const taskInput = document.getElementById('taskInput');
const text = taskInput.value.trim();
if (text) {
tasks.push({ text: text, completed: false });
updateTasksList();
updateStats();
saveTasks();
taskInput.value = "";
}
};
I used .trim() to avoid saving empty or whitespace-only tasks — a small but important detail.
Completing Tasks (Toggle)
Each task has a checkbox. When checked, it toggles the completed boolean on the task object and re-renders the list. Completed tasks display with a teal strikethrough to visually distinguish them from pending ones.
const toggleTastComplete = (index) => {
tasks[index].completed = !tasks[index].completed;
updateTasksList();
updateStats();
saveTasks();
};
The CSS handles the visual:
.completed p {
text-decoration: line-through;
color: var(--teal);
}
Editing Tasks
Clicking the edit icon on any task loads its text back into the input field and removes it from the list. This lets the user modify it and re-add it with the + button. It's a simple but effective pattern.
const editTask = (index) => {
const taskInput = document.getElementById('taskInput');
taskInput.value = tasks[index].text;
tasks.splice(index, 1);
updateTasksList();
updateStats();
saveTasks();
};
Deleting Tasks
A delete icon removes a task permanently from the array using splice(). The list and stats update immediately.
Progress Bar & Stats Counter
This was one of my favourite parts to build. There's a live progress bar that fills up as you complete tasks, and a circular stats counter showing completed / total.
const updateStats = () => {
const completeTasks = tasks.filter(task => task.completed).length;
const totalTasks = tasks.length;
const progress = (completeTasks / totalTasks) * 100;
document.getElementById('progress').style.width = `${progress}%`;
document.getElementById('numbers').innerText = `${completeTasks} / ${totalTasks}`;
};
The progress bar uses a CSS transition for a smooth animation:
#progress {
transition: all 0.3s ease;
}
Confetti Celebration
My favourite Easter egg — when every single task is checked off, the app fires a confetti animation using the @hiseb/confetti CDN library. It's a small touch that makes the app feel rewarding to use.
if (tasks.length && completeTasks === totalTasks) {
blaskConfetti();
}
Persistent Storage with localStorage
Tasks are saved to the browser's localStorage so they survive a page refresh. Every time the user adds, edits, deletes, or completes a task, the updated array is serialized and saved.
const saveTasks = () => {
localStorage.setItem('tasks', JSON.stringify(tasks));
};
On page load, tasks are retrieved and the UI is rebuilt:
document.addEventListener('DOMContentLoaded', function () {
const storedTasks = JSON.parse(localStorage.getItem('tasks'));
if (storedTasks) {
storedTasks.forEach((task) => tasks.push(task));
updateTasksList();
updateStats();
}
});
UI Design Decisions
I went with a deep navy dark theme that feels clean and modern. The colour palette is defined with CSS variables, making it easy to adjust globally:
:root {
--background: #000430;
--secondaryBackground: #171c48;
--text: #fff;
--purple: #828dff;
--teal: #24feee;
}
The purple is used for borders, buttons, and the stats circle. The teal is reserved for active/completed states and the progress bar — creating a clear visual hierarchy. The rounded corners and consistent padding make everything feel polished without overcomplicating the CSS.
5.Challenges I Faced
DOM Re-rendering
Every time a task is added, deleted, edited, or toggled, I clear the entire innerHTML of the task list and re-render everything from scratch. This is simple and effective for small lists, but not efficient for larger ones (more on this in future improvements).
Inline Event Handlers
Since task items are built with innerHTML, I used inline onclick attributes to attach event handlers. This works fine but isn't ideal — a better approach would be event delegation on the parent list element.
Scope Issue
There's a subtle bug in the original code — the stats calculation at the top of the file runs before any tasks exist, so it always computes 0 / 0. Moving all initial logic inside the DOMContentLoaded handler or inside updateStats() (which is already correctly placed) would fix this cleanly.
6.What I Learned
localStorage is incredibly easy to use and a great first step into data persistence before touching databases.
CSS variables make theming clean and maintainable — I'll use them in every project going forward.
State management in JS is about maintaining a single source of truth (the tasks array) and always rendering from it, rather than trying to read state from the DOM.
Small UX details — like transitions, strikethroughs, and confetti — make a huge difference in how a project feels to use.
7.Future Improvements
There's a lot I'd love to add to make this app more production-ready:
Due dates — Add an optional date picker to each task
Priority levels — Mark tasks as Low, Medium, or High priority with colour coding
Task categories / tags — Organise tasks into groups like Work, Personal, Shopping
Backend + user accounts — Move from localStorage to a real database so tasks sync across devices
Recurring tasks — Set tasks to repeat daily, weekly, or monthly
You Can Check My To Do List By Clicking The Below Link
To Do List
8.Final Thoughts
Building this Todo App was a great exercise in keeping things simple while still producing something genuinely useful.JavaScript gets a lot less credit than it deserves — you don't always need a framework to build something clean and functional
Top comments (0)