DEV Community

Cover image for I Built a Todo List App with HTML, CSS & JS — Here's What I Learned
DHANRAJ S
DHANRAJ S

Posted on

I Built a Todo List App with HTML, CSS & JS — Here's What I Learned

Hey!

I built a Todo List app.

Simple? Yes. But this project taught me some JavaScript concepts I was avoiding for a long time.

localStorage. Dynamic DOM. Event listeners. Array methods.

All in one small project.

Let me walk you through what I built — and what actually clicked for me.

You can check out all my frontend projects right here.


1. What This Todo App Does

  • Type a task and press Add
  • Task appears in the list below
  • Click a task → it gets a strikethrough (marked as done)
  • Press Delete → task is removed
  • Refresh the page → tasks are still there (thanks to localStorage)

That last point is the interesting one. Let's get into it.


2. The HTML — Just the Skeleton

<input id="inputText" type="text" placeholder="Enter your task" />
<button class="add-btn" id="btn">Add</button>

<ul id="list"></ul>
Enter fullscreen mode Exit fullscreen mode

That's honestly all the HTML needed.

<input id="inputText"> → where you type the task.

<button id="btn"> → the Add button.

<ul id="list"> → the list that shows all tasks. Starts empty. JavaScript fills it in.

No tasks are hardcoded in HTML. Everything is created dynamically by JS. That was new for me.


3. The CSS — Two Things I Really Liked

The gradient background:

body {
  background:
    radial-gradient(at top left, #9d61ba, transparent),
    radial-gradient(at bottom right, #718777, #554f75);
}
Enter fullscreen mode Exit fullscreen mode

Two radial-gradient layers stacked together — purple top left, green bottom right. They blend in the middle. Looks way better than a solid color.

The gradient text on the heading:

h1 {
  background: linear-gradient(135deg, #f97aff 0%, #7afff0 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
Enter fullscreen mode Exit fullscreen mode

Quick question for you

How do you make text show a gradient color?

You apply a gradient as the background — then clip it to the text shape — then make the text color transparent so the background shows through.

Three lines working together. The result? Pink to cyan gradient text.


4. The JavaScript — This Is Where I Learned the Most

Instead of dumping the full code at once — let me show you how the app is broken into 4 clear responsibilities:

What it does Function
Save tasks to browser saveTodos()
Draw tasks on screen renderTodos()
Add a new task btn click event
Load tasks on startup renderTodos() called once at the end

Every action in this app goes through these 4 things.

Add a task → save → redraw.
Delete a task → save → redraw.
Mark done → save → redraw.

Simple pattern. Repeated every time. Once I saw that — the whole JS made sense.

Let's go through the parts that taught me the most.


5. localStorage — Tasks That Don't Disappear

This was the biggest thing I learned in this project.

Normally — when you refresh a page, everything resets. Variables are gone. Your tasks vanish.

localStorage fixes that. It saves data in the browser — and it stays there even after a refresh.

Saving data:

localStorage.setItem("todos", JSON.stringify(todos));
Enter fullscreen mode Exit fullscreen mode

localStorage only stores strings. So JSON.stringify() converts our array into a string first.

todos (array) → JSON.stringify"[{...},{...}]" (string) → saved in browser.

Loading data:

let todos = JSON.parse(localStorage.getItem("todos")) || [];
Enter fullscreen mode Exit fullscreen mode

localStorage.getItem("todos") → gets the saved string back.

JSON.parse() → converts it back into a real array.

|| [] → if nothing is saved yet, start with an empty array.

Quick question for you

Why do we need both JSON.stringify and JSON.parse?

Because localStorage only understands strings — not arrays or objects. So we convert to string when saving, and convert back when loading.

String in. String out. JS array in between.


6. renderTodos() — Drawing the List Every Time

function renderTodos() {
  ul.innerHTML = "";

  todos.forEach((todo, index) => {
    const li = document.createElement("li");
    // ...build each task item
    ul.appendChild(li);
  });
}
Enter fullscreen mode Exit fullscreen mode

Every time something changes — add, delete, mark done — we call renderTodos().

It clears the list with ul.innerHTML = "" and redraws everything fresh.

document.createElement("li") → creates a new <li> element in JavaScript. No HTML needed.

ul.appendChild(li) → adds it to the page.

I never created HTML elements from JavaScript before this project. Now it feels natural.


7. Marking a Task as Done — Toggle Logic

li.addEventListener("click", () => {
  todos[index].done = !todos[index].done;
  saveTodos();
  renderTodos();
});
Enter fullscreen mode Exit fullscreen mode

When you click a task — it toggles between done and not done.

!todos[index].done → if it's true, make it false. If it's false, make it true.

That ! flips the value every time. One line. Clean toggle.

In CSS, when a task is done — we add the class done which adds a strikethrough:

li.done span {
  text-decoration: line-through;
  color: #9ca3af;
}
Enter fullscreen mode Exit fullscreen mode

JS handles the logic. CSS handles the look. They work together perfectly.


8. Deleting a Task — splice()

delBtn.addEventListener("click", () => {
  todos.splice(index, 1);
  saveTodos();
  renderTodos();
});
Enter fullscreen mode Exit fullscreen mode

todos.splice(index, 1) → removes 1 item from the array at position index.

So if you have ["Buy milk", "Read book", "Go gym"] and delete index 1:

Result → ["Buy milk", "Go gym"].

"Read book" is gone. Array updated. Save. Redraw. Done.


9. The Add Button — Putting It All Together

btn.addEventListener("click", () => {
  const value = inputText.value.trim();
  if (value === "") return;

  todos.push({ text: value, done: false });
  saveTodos();
  renderTodos();
  inputText.value = "";
});
Enter fullscreen mode Exit fullscreen mode

.trim() → removes empty spaces from the start and end. So just pressing spacebar doesn't add a blank task.

if (value === "") return → if the input is empty, stop. Don't add anything.

todos.push({ text: value, done: false }) → add a new task object to the array. Every task has a text and a done status.

Then save → redraw → clear the input.

Every. Single. Time.


10. What This Project Actually Taught Me

  • localStorage — saving and loading data in the browser. JSON.stringify to save. JSON.parse to load. I use this in every project now.

  • document.createElement() — building HTML elements with JavaScript instead of hardcoding them. Powerful once it clicks.

  • splice() — removing items from an array by index. Simple but I always forgot how it worked before this.

  • Toggle with ! — flipping a boolean value in one character. Clean and satisfying.

  • renderTodos() pattern — clear and redraw. Simple state management without any library.

  • Gradient text with CSSbackground-clip: text + transparent fill. Looks great. One trick I'll use everywhere now.


Quick Summary

  1. localStorage.setItem + JSON.stringify → saves array as string in browser
  2. localStorage.getItem + JSON.parse → loads it back as a real array
  3. document.createElement() → builds HTML elements from JavaScript
  4. Every action follows one pattern → save → redraw → done
  5. !todo.done → toggles between true and false
  6. renderTodos() → clears and redraws the list every time something changes

You can see my project here: https://dhanraj166.github.io/frontend-projects/todolist.html.

Build this yourself. It's the perfect beginner project.

If you build your own version — drop it in the comments. I'd love to see what you make.


Thanks for reading! Keep building — one project at a time.

Top comments (0)