Navigation

Programming

Building Your First Todo App: From "Hello World" to "Oh Wow, I Built Something!"

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? Sett...
Jul 05, 2025
12 min read

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:

  1. Add Tasks: Type and press Enter or click Add
  2. Complete Tasks: Click the checkbox or the text
  3. Delete Tasks: Hover and click delete
  4. Filter Views: All, Active, Completed
  5. Persistence: Refreshing the page keeps your todos
  6. Stats: See how many tasks left
  7. 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:

  1. Start Simple: My first version just added and displayed todos. That’s it.
  2. Iterate: Each feature taught me something new
  3. Break Things: Deleting the wrong todo taught me about unique IDs
  4. Ask for Help: My classmate pointed out the XSS vulnerability
  5. 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! 🧋✨

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Programming