After years of production experience, I've compiled the most important patterns and techniques for building a to-do app. Here's everything you need to know.
Project Setup
We're building a fully functional to-do app with vanilla JavaScript — no frameworks, no libraries. Here's what it'll do:
- Add, edit, and delete tasks
- Mark tasks as complete
- Filter by status (All / Active / Completed)
- Persist data with localStorage
- Search tasks by text
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DRIXO Todo App</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; background: #1a1a2e; color: #e2e8f0; min-height: 100vh; padding: 40px 20px; }
.app { max-width: 600px; margin: 0 auto; }
h1 { text-align: center; color: #00d4aa; margin-bottom: 30px; }
.input-group { display: flex; gap: 10px; margin-bottom: 20px; }
.input-group input { flex: 1; padding: 12px 16px; border: 2px solid #2d3748; border-radius: 8px; background: #0f0f23; color: #e2e8f0; font-size: 16px; }
.input-group button { padding: 12px 24px; background: #00d4aa; color: #1a1a2e; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; }
.filters { display: flex; gap: 8px; margin-bottom: 20px; }
.filters button { padding: 8px 16px; border: 1px solid #2d3748; border-radius: 6px; background: transparent; color: #94a3b8; cursor: pointer; }
.filters button.active { background: #00d4aa; color: #1a1a2e; border-color: #00d4aa; }
.task { display: flex; align-items: center; gap: 12px; padding: 14px; background: #16213e; border-radius: 8px; margin-bottom: 8px; }
.task.done span { text-decoration: line-through; opacity: 0.5; }
.task span { flex: 1; }
.task button { background: none; border: none; color: #ef4444; cursor: pointer; font-size: 18px; }
.count { text-align: center; color: #64748b; margin-top: 16px; }
</style>
</head>
<body>
<div class="app">
<h1>Todo App</h1>
<div class="input-group">
<input type="text" id="taskInput" placeholder="What needs to be done?">
<button onclick="addTask()">Add</button>
</div>
<input type="text" id="searchInput" placeholder="Search tasks..." style="width:100%;padding:10px 16px;border:2px solid #2d3748;border-radius:8px;background:#0f0f23;color:#e2e8f0;margin-bottom:16px;">
<div class="filters" id="filters"></div>
<div id="taskList"></div>
<div class="count" id="count"></div>
</div>
<script src="app.js"></script>
</body>
</html>
JavaScript: Task Management
Now let's build the JavaScript logic. We'll manage tasks as an array of objects:
// app.js — Task Management Logic
let tasks = JSON.parse(localStorage.getItem('todos')) || [];
let currentFilter = 'all';
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
function addTask() {
const input = document.getElementById('taskInput');
const text = input.value.trim();
if (!text) return;
tasks.push({
id: generateId(),
text: text,
done: false,
createdAt: new Date().toISOString()
});
input.value = '';
save();
render();
}
function toggleTask(id) {
const task = tasks.find(t => t.id === id);
if (task) task.done = !task.done;
save();
render();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
save();
render();
}
function editTask(id) {
const task = tasks.find(t => t.id === id);
if (!task) return;
const newText = prompt('Edit task:', task.text);
if (newText !== null && newText.trim()) {
task.text = newText.trim();
save();
render();
}
}
// Listen for Enter key
document.getElementById('taskInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTask();
});
Adding Local Storage
We persist tasks to localStorage so they survive page refreshes:
function save() {
localStorage.setItem('todos', JSON.stringify(tasks));
}
function render() {
const list = document.getElementById('taskList');
const search = document.getElementById('searchInput').value.toLowerCase();
let filtered = tasks;
// Apply filter
if (currentFilter === 'active') filtered = filtered.filter(t => !t.done);
if (currentFilter === 'completed') filtered = filtered.filter(t => t.done);
// Apply search
if (search) filtered = filtered.filter(t => t.text.toLowerCase().includes(search));
// Render tasks
list.innerHTML = filtered.map(task => `
<div class="task ${task.done ? 'done' : ''}">
<input type="checkbox" ${task.done ? 'checked' : ''} onchange="toggleTask('${task.id}')">
<span ondblclick="editTask('${task.id}')">${escapeHtml(task.text)}</span>
<button onclick="deleteTask('${task.id}')">×</button>
</div>
`).join('');
// Update count
const active = tasks.filter(t => !t.done).length;
document.getElementById('count').textContent =
`${active} task${active !== 1 ? 's' : ''} remaining`;
renderFilters();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
Filtering and Search
function renderFilters() {
const container = document.getElementById('filters');
const filters = ['all', 'active', 'completed'];
container.innerHTML = filters.map(f => `
<button class="${f === currentFilter ? 'active' : ''}"
onclick="setFilter('${f}')">
${f.charAt(0).toUpperCase() + f.slice(1)}
(${getCount(f)})
</button>
`).join('');
// Add "Clear Completed" if any completed tasks exist
if (tasks.some(t => t.done)) {
container.innerHTML += `
<button onclick="clearCompleted()" style="margin-left:auto;color:#ef4444;border-color:#ef4444;">
Clear Completed
</button>`;
}
}
function getCount(filter) {
if (filter === 'all') return tasks.length;
if (filter === 'active') return tasks.filter(t => !t.done).length;
return tasks.filter(t => t.done).length;
}
function setFilter(filter) {
currentFilter = filter;
render();
}
function clearCompleted() {
tasks = tasks.filter(t => !t.done);
save();
render();
}
// Search listener
document.getElementById('searchInput').addEventListener('input', render);
// Initial render
render();
Complete Code
That's it! You now have a complete, functional to-do app built with vanilla JavaScript. Key concepts we covered:
- DOM manipulation — dynamically creating and updating HTML
- Event handling — click, keypress, change, input events
- localStorage — persisting data across sessions
- Array methods — filter, map, find, some
- Template literals — building HTML strings
Next steps: Try adding drag-and-drop reordering, due dates, or categories to extend this app further.
Full-Stack Developer & Technical Writer at DRIXO
Full-stack developer with 5+ years of experience in Python and JavaScript. I love breaking down complex concepts into simple, practical tutorials. When I'm not coding, you'll find me contributing to open-source projects.
Comments