A clear, step-by-step guide to building a small to-do app: plan the features, create the screens, add logic, save data, test, and publish.

When I say “app” in this guide, I mean a small web app: a single page you open in a browser that responds to what you click and type. No installs, no accounts, no heavy setup—just a simple project you can run locally.
By the end, you’ll have a to‑do app that can:
localStorage (so closing the tab doesn’t wipe everything)It won’t be perfect or “enterprise-grade,” and that’s the point. This is a beginner project designed to teach the basics without throwing lots of tools at you.
You’ll build the app step by step and pick up the core pieces of how front-end web apps work:
Keep it simple. You only need:
If you can create a folder and edit a few files, you’re ready.
Before you write any code, decide what “success” looks like. This tutorial builds one small app with one clear job: help you track tasks you want to do.
Write a one-sentence goal you can keep in front of you while building:
“This app lets me add tasks to a list so I don’t forget them.”
That’s it. If you feel tempted to add calendars, reminders, tags, or accounts, park those ideas for later.
Make two quick lists:
Must-have (for this project):
Nice-to-have (not required today): due dates, priorities, categories, search, drag-and-drop, cloud sync.
Keeping “must-have” small helps you actually finish.
This app can be a single page with:
Be specific so you don’t get stuck:
With that decided, you’re ready to set up the project files.
Before we write any code, let’s create a clean little “home” for the app. Keeping files organized from the start makes the next steps smoother.
Make a new folder on your computer and name it something like todo-app. This folder will hold everything for this project.
Inside that folder, create three files:
index.html (the page structure)styles.css (the look and layout)app.js (the behavior and interactivity)If your computer hides file extensions (like “.html”), make sure you’re actually creating real files. A common beginner mistake is ending up with index.html.txt.
Open the todo-app folder in your code editor (VS Code, Sublime Text, etc.). Then open index.html in your web browser.
At this point, your page may be blank—and that’s fine. We’ll add content in the next step.
When you edit your files, your browser won’t automatically update (unless you use a tool that does that).
So the basic loop is:
If something “doesn’t work,” refresh is the first thing to try.
You can build this app by double‑clicking index.html, but a local server can prevent weird issues later (especially when you start saving data or loading files).
Beginner-friendly options:
python -m http.server
Then open the address it prints (often http://localhost:8000) in your browser.
Now we’ll create a clean skeleton for the app. This HTML won’t make anything interactive yet (that’s next), but it gives your JavaScript clear places to read from and write to.
We’ll include:
Keep the names simple and readable. Good IDs/classes make later steps easier because your JavaScript can grab elements by name without confusion.
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>To‑Do App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="app" id="app">
<h1 class="app__title" id="appTitle">My To‑Do List</h1>
<form class="task-form" id="taskForm">
<label class="task-form__label" for="taskInput">New task</label>
<div class="task-form__row">
<input
id="taskInput"
class="task-form__input"
type="text"
placeholder="e.g., Buy milk"
autocomplete="off"
/>
<button id="addButton" class="task-form__button" type="submit">
Add
</button>
</div>
</form>
<ul class="task-list" id="taskList" aria-label="Task list"></ul>
</main>
<script src="app.js"></script>
</body>
</html>
That’s it for structure. Notice we used id="taskInput" and id="taskList"—those are the two elements you’ll talk to most in JavaScript.
Right now your page exists, but it probably looks like a plain document. A little CSS makes it easier to use: clearer spacing, readable text, and buttons that feel clickable.
A centered box keeps the app focused and stops the content from stretching across a wide screen.
/* Basic page setup */
body {
font-family: Arial, sans-serif;
background: #f6f7fb;
margin: 0;
padding: 24px;
}
/* Centered app container */
.container {
max-width: 520px;
margin: 0 auto;
background: #ffffff;
padding: 16px;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
}
Each task should look like a separate “row,” with comfortable spacing.
ul { list-style: none; padding: 0; margin: 16px 0 0; }
li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid #e7e7ee;
border-radius: 8px;
margin-bottom: 10px;
}
.task-text { flex: 1; }
When a task is completed, it should visually change so you can tell at a glance.
.done .task-text {
text-decoration: line-through;
color: #777;
opacity: 0.85;
}
Keep buttons the same size and style so they feel like part of one app.
button {
border: none;
border-radius: 8px;
padding: 8px 10px;
cursor: pointer;
}
button:hover { filter: brightness(0.95); }
That’s enough styling for a clean, friendly UI—no advanced tricks needed. Next, we’ll wire up behavior with JavaScript.
Now that you have the input, button, and list on the page, you’ll make them do something. The goal is simple: when someone types a task and hits Add (or presses Enter), a new item appears in the list.
In your JavaScript file, first grab the elements you need, then listen for two actions: a button click and the Enter key inside the input.
const taskInput = document.querySelector('#taskInput');
const addBtn = document.querySelector('#addBtn');
const taskList = document.querySelector('#taskList');
function addTask() {
const text = taskInput.value.trim();
// Block empty tasks (including ones that are just spaces)
if (text === '') return;
const li = document.createElement('li');
li.textContent = text;
taskList.appendChild(li);
// Clear the input and put the cursor back
taskInput.value = '';
taskInput.focus();
}
addBtn.addEventListener('click', addTask);
taskInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
addTask();
}
});
trim() to remove extra spaces at the start/end.<li>, sets its text, and adds it to the list.If nothing happens, double-check your element IDs in the HTML match your JavaScript selectors exactly (this is one of the most common beginner hiccups).
Now that you can add tasks, let’s make them actionable: you should be able to mark a task as done and remove it.
Instead of storing tasks as plain strings, store them as objects. That gives each task a stable identity and a place to track “done” status:
text: what the task saysdone: true or falseid: a unique number so we can find/delete the right taskHere’s a simple example:
let tasks = [
{ id: 1, text: "Buy milk", done: false },
{ id: 2, text: "Email Sam", done: true }
];
When you render each task on the page, include either a checkbox or a “Done” button, plus a “Delete” button.
An event listener is just “a way to react to clicks.” You attach it to a button (or the whole list), and when the user clicks, your code runs.
A beginner-friendly pattern is event delegation: put one click listener on the task list container, then check what was clicked.
function toggleDone(id) {
tasks = tasks.map(t => t.id === id ? { ...t, done: !t.done } : t);
renderTasks();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
renderTasks();
}
document.querySelector("#taskList").addEventListener("click", (e) => {
const id = Number(e.target.dataset.id);
if (e.target.matches(".toggle")) toggleDone(id);
if (e.target.matches(".delete")) deleteTask(id);
});
In your renderTasks() function:
data-id="${task.id}" to each button..done class).Right now your to‑do list has an annoying problem: if you refresh the page (or close the tab), everything disappears.
That happens because your tasks only exist in JavaScript memory. When the page reloads, that memory resets.
localStorage is built into the browser. Think of it like a small box where you can store text under a name (a “key”). It’s perfect for beginner projects because it doesn’t need a server or account system.
We’ll store the entire task list as JSON text, then load it back when the page opens.
Any time you add a task, mark one done, or delete one, call saveTasks().
const STORAGE_KEY = "todo.tasks";
function saveTasks(tasks) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
Wherever your app updates the tasks array, do this right after:
saveTasks(tasks);
renderTasks(tasks);
When the page loads, read the saved value. If there’s nothing saved yet, fall back to an empty list.
function loadTasks() {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
}
let tasks = loadTasks();
renderTasks(tasks);
That’s it: your app now remembers tasks across refreshes.
Tip: localStorage only stores strings, so JSON.stringify() turns your array into text, and JSON.parse() turns it back into a real array when you load it.
Testing sounds boring, but it’s the fastest way to turn your to‑do app from “works on my machine” into “works every time.” Do one quick pass after each small change.
Run through the main flow in this order:
If any step fails, fix it before adding new features. Small apps get messy when you stack problems.
Edge cases are inputs that you didn’t design for, but real people will do anyway:
A common fix is to block empty tasks:
const text = input.value.trim();
addButton.disabled = text === "";
(You can run that on every input event, and again right before you add.)
When clicks do nothing, it’s usually one of these:
id or class name differs between HTML and JS).app.js not found).When something feels random, log it:
console.log("Adding:", text);
console.log("Current tasks:", tasks);
Check the browser’s Console for errors (red text). Once you’ve fixed the issue, remove the logs so they don’t clutter your project.
A to‑do app is only “done” when it’s comfortable for real people to use—on phones, with a keyboard, and with assistive tools like screen readers.
On small screens, tiny buttons are frustrating. Give clickable things enough space:
If you’re using CSS, increasing padding, font-size, and gap often makes the biggest difference.
Screen readers need clear names for controls.
<label> (best option). If you don’t want to show it, you can visually hide it with CSS, but keep it in the HTML.aria-label="Delete task" so the screen reader doesn’t announce it as “button” with no context.This helps people understand what each control does without guessing.
Make sure you can use the whole app without a mouse:
<form> so Enter works naturally).Use a readable font size (16px is a good baseline) and strong color contrast (dark text on a light background, or the reverse). Avoid using color alone to show “done”—add a clear style like a strikethrough plus a “Done” state.
Now that everything works, take 10–15 minutes to tidy up. This makes future fixes easier and helps you understand your own project when you come back later.
Keep it small and predictable:
/index.html — the page structure (input, button, list)/styles.css — how the app looks (spacing, fonts, “done” style)/app.js — the behavior (add, toggle done, delete, save/load)/README.md — quick notes for “future you”If you prefer subfolders, you can also do:
/css/styles.css/js/app.jsJust make sure your <link> and <script> paths match.
A few quick wins:
taskInput, taskList, saveTasks()For example, it’s easier to scan:
renderTasks(tasks)addTask(text)toggleTask(id)deleteTask(id)Your README.md can be simple:
index.html in a browser)At minimum, zip the folder after you finish a milestone (like “localStorage works”). If you want version history, Git is great—but optional. Even one backup copy can save you from accidental deletions.
Publishing just means putting your app’s files (HTML, CSS, JavaScript) somewhere public on the internet so other people can open a link and use it. Since this to‑do app is a “static site” (it runs in the browser and doesn’t need a server), you can host it for free on several services.
High-level steps:
If your app uses separate files, double-check your file names match your links exactly (for example styles.css vs style.css).
If you want the simplest “upload and go” approach:
localStorage working).Once it passes, send the link to a friend and ask them to try it—fresh eyes catch issues fast.
You’ve built a working to‑do app. If you want to keep learning without jumping to a huge project, these upgrades add real value and teach useful patterns.
Add an “Edit” button next to each task. When clicked, swap the task label for a small input field (pre-filled), plus “Save” and “Cancel.”
Tip: keep your task data as an array of objects (with an id and text). Editing then becomes: find the right task by id, update text, re-render, and save.
Add three buttons at the top: All, Active, Done.
Store the current filter in a variable like currentFilter = 'all'. When rendering, show:
Keep it lightweight:
YYYY-MM-DD) and display it next to the taskEven one extra field teaches you how to update your data model and UI together.
When you’re ready, the big idea is: instead of saving to localStorage, you send tasks to an API (a server) using fetch(). The server stores them in a database, so tasks sync across devices.
If you want to try that jump without rebuilding everything from scratch, a vibe-coding platform like Koder.ai can help you prototype the “next version” quickly: describe the features in chat (API endpoints, database tables, UI changes), iterate in planning mode, and still export the source code when you’re ready to learn from (or customize) the generated React/Go/PostgreSQL project.
Try building a notes app (with search) or a habit tracker (daily check-ins). They reuse the same skills: list rendering, editing, saving, and simple UI design.