Welcome to DRIXO — Your Coding Journey Starts Here
DRIXO Code • Learn • Build

Building a To-Do App with Vanilla JavaScript

January 13, 2026 8 min read 0 Comments
Building a To-Do App with Vanilla JavaScript
JavaScript

Building a To-Do App with Vanilla JavaScript

DRIXO

Code · Learn · Build

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}')">&times;</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();
Pro Tip: Double-click any task to edit it. The app auto-saves to localStorage after every change.

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.

AM
Arjun Mehta
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