- title: Building Your First Todo App: From “Hello World” to “Oh Wow, I Built Something!”summary: Build your first todo app with HTML, CSS, and JavaScript while learning essential web development concepts through a complete, practical project that actually works and looks modern.
- Why a Todo App?
- Setting Up: The Coffee Shop Setup
- The HTML: Building Our Bento Box
- The CSS: Making It Not Look Like 1995
- The JavaScript: Where the Magic Happens
- Features We Built
- Enhancements: Making It Yours
- Common Mistakes (I Made Them All)
- Deployment: Showing the World
- What I Learned Building This
- Your Turn!
- Final Thoughts
title: Building Your First Todo App: From “Hello World” to “Oh Wow, I Built Something!” summary: Build your first todo app with HTML, CSS, and JavaScript while learning essential web development concepts through a complete, practical project that actually works and looks modern.
Building Your First Todo App: From “Hello World” to “Oh Wow, I Built Something!”
Three years ago, I was sitting in my UW dorm room at 1 AM, surrounded by empty bubble tea cups and crumpled post-it notes with my actual todos. That’s when it hit me - why not build a digital version? That first todo app changed everything. It wasn’t pretty (the CSS still haunts me), but it worked, and more importantly, it made me feel like a “real” developer.
Today, let’s build a todo app together. Not because the world needs another todo app, but because it’s the perfect first project. It’s like the dan dan noodles of programming - simple ingredients, but when done right, deeply satisfying.
Why a Todo App?
Every developer builds a todo app. It’s our rite of passage, like:
- Spilling coffee on your keyboard (twice)
- Googling “how to exit vim” (still do this)
- Pushing to the wrong branch (last week)
But seriously, todo apps teach you everything:
- User input (adding tasks)
- State management (tracking tasks)
- CRUD operations (Create, Read, Update, Delete)
- DOM manipulation (showing tasks)
- Event handling (clicking, typing)
- Local storage (persistence)
Setting Up: The Coffee Shop Setup
First, let’s create our workspace. I’m at Lighthouse Roasters in Fremont (excellent cortado), and here’s our setup:
# Create project structure
mkdir my-todo-app
cd my-todo-app
# Create files
touch index.html
touch style.css
touch script.js
# Open in VS Code (or your editor)
code .
The HTML: Building Our Bento Box
Think of HTML as the bento box structure - compartments for everything:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maya's Todo App (Finally Organized!)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>✨ Today's Missions</h1>
<!-- Input Section -->
<div class="input-section">
<input
type="text"
id="todoInput"
placeholder="What needs doing? (probably coffee first...)"
autocomplete="off"
>
<button id="addButton">Add Task</button>
</div>
<!-- Filter Buttons -->
<div class="filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="completed">Completed</button>
</div>
<!-- Todo List -->
<ul id="todoList" class="todo-list">
<!-- Tasks will appear here -->
</ul>
<!-- Stats -->
<div class="stats">
<span id="itemsLeft">0 items left</span>
<button id="clearCompleted">Clear Completed</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
The CSS: Making It Not Look Like 1995
Remember my first todo app? Gray background, Times New Roman, no spacing. My designer friend took one look and said “Maya, this hurts.” So here’s better styling:
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* Container - Our Main Bento Box */
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
padding: 40px 30px;
}
h1 {
color: #2d3748;
text-align: center;
margin-bottom: 30px;
font-size: 2rem;
}
/* Input Section */
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todoInput {
flex: 1;
padding: 12px 20px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
#todoInput:focus {
outline: none;
border-color: #667eea;
}
#addButton {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
#addButton:hover {
background: #5a67d8;
}
/* Filter Buttons */
.filters {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.filter-btn {
padding: 8px 16px;
background: transparent;
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* Todo List */
.todo-list {
list-style: none;
margin-bottom: 20px;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #e2e8f0;
transition: all 0.3s;
}
.todo-item:hover {
background: #f7fafc;
}
.todo-checkbox {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
.todo-text {
flex: 1;
color: #2d3748;
cursor: pointer;
}
.todo-text.completed {
text-decoration: line-through;
color: #a0aec0;
}
.delete-btn {
background: #fc8181;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
}
.todo-item:hover .delete-btn {
opacity: 1;
}
/* Stats Section */
.stats {
display: flex;
justify-content: space-between;
align-items: center;
color: #718096;
font-size: 14px;
}
#clearCompleted {
background: transparent;
border: none;
color: #e53e3e;
cursor: pointer;
text-decoration: underline;
}
The JavaScript: Where the Magic Happens
This is where I spent most of my time that night in the dorm. Here’s the JavaScript that brings it all to life:
// Our Todo App Brain
class TodoApp {
constructor() {
// State - like my actual desk, but organized
this.todos = this.loadTodos();
this.filter = 'all';
// Cache DOM elements
this.todoInput = document.getElementById('todoInput');
this.addButton = document.getElementById('addButton');
this.todoList = document.getElementById('todoList');
this.itemsLeft = document.getElementById('itemsLeft');
this.clearCompletedBtn = document.getElementById('clearCompleted');
this.filterButtons = document.querySelectorAll('.filter-btn');
// Bind events
this.initEventListeners();
// Initial render
this.render();
}
// Load todos from localStorage (survive page refresh!)
loadTodos() {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
}
// Save todos to localStorage
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
// Initialize event listeners
initEventListeners() {
// Add todo on button click
this.addButton.addEventListener('click', () => this.addTodo());
// Add todo on Enter key
this.todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.addTodo();
});
// Filter buttons
this.filterButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
this.setFilter(e.target.dataset.filter);
});
});
// Clear completed
this.clearCompletedBtn.addEventListener('click', () => {
this.clearCompleted();
});
}
// Add a new todo
addTodo() {
const text = this.todoInput.value.trim();
// Don't add empty todos (learned this the hard way)
if (!text) {
this.todoInput.placeholder = "Come on, type something! 😅";
setTimeout(() => {
this.todoInput.placeholder = "What needs doing? (probably coffee first...)";
}, 2000);
return;
}
// Create todo object
const todo = {
id: Date.now(), // Simple ID generation
text: text,
completed: false,
createdAt: new Date().toISOString()
};
// Add to array
this.todos.unshift(todo); // New todos first, like my post-its
// Clear input
this.todoInput.value = '';
// Save and render
this.saveTodos();
this.render();
}
// Toggle todo completion
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.saveTodos();
this.render();
}
}
// Delete a todo
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.saveTodos();
this.render();
}
// Set active filter
setFilter(filter) {
this.filter = filter;
// Update active button
this.filterButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
this.render();
}
// Clear completed todos
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
this.saveTodos();
this.render();
}
// Get filtered todos
getFilteredTodos() {
switch(this.filter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
// Render the todo list
render() {
// Clear current list
this.todoList.innerHTML = '';
// Get filtered todos
const filteredTodos = this.getFilteredTodos();
// Render each todo
filteredTodos.forEach(todo => {
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? 'checked' : ''}
data-id="${todo.id}"
>
<span class="todo-text ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
${this.escapeHtml(todo.text)}
</span>
<button class="delete-btn" data-id="${todo.id}">Delete</button>
`;
// Add event listeners to the new elements
const checkbox = li.querySelector('.todo-checkbox');
const text = li.querySelector('.todo-text');
const deleteBtn = li.querySelector('.delete-btn');
checkbox.addEventListener('change', () => this.toggleTodo(todo.id));
text.addEventListener('click', () => this.toggleTodo(todo.id));
deleteBtn.addEventListener('click', () => this.deleteTodo(todo.id));
this.todoList.appendChild(li);
});
// Update stats
this.updateStats();
}
// Update statistics
updateStats() {
const activeTodos = this.todos.filter(t => !t.completed).length;
this.itemsLeft.textContent = `${activeTodos} ${activeTodos === 1 ? 'item' : 'items'} left`;
// Show/hide clear completed button
const hasCompleted = this.todos.some(t => t.completed);
this.clearCompletedBtn.style.display = hasCompleted ? 'block' : 'none';
}
// Escape HTML to prevent XSS (security matters!)
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const app = new TodoApp();
// Add some default todos for first-time users
if (app.todos.length === 0) {
app.todoInput.value = "Build my first todo app ✅";
app.addTodo();
app.todoInput.value = "Celebrate with bubble tea 🧋";
app.addTodo();
app.todoInput.value = "Show mom what I built";
app.addTodo();
}
});
Features We Built
Our todo app now has:
- Add Tasks: Type and press Enter or click Add
- Complete Tasks: Click the checkbox or the text
- Delete Tasks: Hover and click delete
- Filter Views: All, Active, Completed
- Persistence: Refreshing the page keeps your todos
- Stats: See how many tasks left
- Clear Completed: Bulk cleanup
Enhancements: Making It Yours
Here are some ideas to level up your todo app:
Add Due Dates
// In the todo object
const todo = {
id: Date.now(),
text: text,
completed: false,
dueDate: null, // Add this
createdAt: new Date().toISOString()
};
// Add date input in HTML
<input type="date" id="dueDate">
Add Categories
// Predefined categories
const categories = ['Work', 'Personal', 'Shopping', 'Bubble Tea Runs'];
// Add to todo object
category: 'Personal'
Add Drag and Drop
// Make items draggable
li.draggable = true;
li.addEventListener('dragstart', handleDragStart);
li.addEventListener('dragover', handleDragOver);
li.addEventListener('drop', handleDrop);
Common Mistakes (I Made Them All)
Not Escaping User Input
My friend typed <script>alert('hacked!')</script>
as a todo. Lesson learned.
Not Handling Edge Cases
Empty todos, super long text, special characters - test everything!
Forgetting Mobile
Half your users are on phones. Make those buttons thumb-friendly!
Deployment: Showing the World
The best part? Deploying is free and easy:
# Using GitHub Pages
git init
git add .
git commit -m "My first todo app!"
git remote add origin YOUR_GITHUB_REPO
git push -u origin main
# Enable GitHub Pages in repo settings
Or use Netlify, Vercel, or even host it on your own domain!
What I Learned Building This
That night in my dorm room, debugging why my todos disappeared on refresh (forgot localStorage), I learned more than any tutorial taught me:
- Start Simple: My first version just added and displayed todos. That’s it.
- Iterate: Each feature taught me something new
- Break Things: Deleting the wrong todo taught me about unique IDs
- Ask for Help: My classmate pointed out the XSS vulnerability
- Ship It: Perfect is the enemy of done
Your Turn!
Now it’s your turn. Build this todo app. Break it. Fix it. Make it yours. Add features I didn’t think of.
Some challenges:
- Add keyboard shortcuts (Delete key, Cmd+Enter)
- Add animations (todos sliding in/out)
- Add themes (dark mode is mandatory in Seattle)
- Add sync across devices
- Add a pomodoro timer
- Add weather integration (todos for sunny days?)
Final Thoughts
Three years later, I still use a version of this todo app. It’s evolved (now it syncs with my phone and has way too many features), but the core is the same code from that late night.
Building a todo app isn’t about the app itself. It’s about the journey. It’s about that moment when you add your first todo, check it off, and realize “I built this.” That feeling? That’s why we code.
So grab your beverage of choice (bubble tea for me), open your editor, and start building. And when you finish, add “Build my first app” to your todo list just so you can check it off.
You’ve got this!
Built this tutorial at the Seattle Public Library (10th floor has the best views). When you build your todo app, tweet me @maya_codes_pnw with a screenshot. I love seeing different approaches and creative features. Bonus points if you add a bubble tea tracker! 🧋✨
Add Comment
No comments yet. Be the first to comment!