Smart To-Do List with LocalStorage

A full-featured task manager with local storage persistence, filtering (All, Active, Completed), task counter, and priority levels.

Difficulty: Beginner Est. Time: 25 mins Vanilla JS (ES6+) Includes HTML, CSS, JS & README (.ZIP)
Live UI Runner

Smart To-Do List with LocalStorage

live-sandbox://todo-list

1. Project Overview & Features

The Smart To-Do List with LocalStorage is an essential frontend project designed to help you bridge the gap between learning JavaScript syntax and building interactive, responsive web applications.

Key Features Implemented:

  • Add new tasks with description and priority tag (High, Normal, Low)
  • Multi-trigger input: Click Add button or press Enter key
  • Mark tasks as completed with strikethrough animation
  • Filter by All, Active, and Completed tasks
  • Real-time remaining task counter
  • Persistent storage with safe sandbox fallback
  • Clear completed tasks in one click

2. Step-by-Step Code Walkthrough

A Step 1: Semantic HTML Structure

We structure the interface using clean, semantic HTML elements. This ensures maximum accessibility and provides intuitive hooks for CSS classes and JavaScript query selectors.

HTML (index.html)
<div class="todo-app">
  <header class="app-header">
    <div class="header-icon"><i class="fas fa-check-double"></i></div>
    <div>
      <h1>TaskFlow</h1>
      <p class="subtitle">Stay organized and get things done</p>
    </div>
  </header>

  <form id="todo-form" class="todo-form" autocomplete="off">
    <div class="input-group">
      <input type="text" id="todo-input" placeholder="What needs to be done?" required autocomplete="off" spellcheck="false">
      <select id="todo-priority" title="Task Priority">
        <option value="normal">Normal</option>
        <option value="high">High</option>
        <option value="low">Low</option>
      </select>
      <button type="submit" id="add-btn" class="btn-primary" title="Add Task">
        <i class="fas fa-plus"></i> <span>Add</span>
      </button>
    </div>
  </form>

  <div class="controls-bar">
    <div class="filter-tabs">
      <button type="button" class="tab-btn active" data-filter="all">All</button>
      <button type="button" class="tab-btn" data-filter="active">Active</button>
      <button type="button" class="tab-btn" data-filter="completed">Completed</button>
    </div>
    <button type="button" id="clear-completed-btn" class="btn-clear">
      <i class="fas fa-trash-alt me-1"></i> Clear Completed
    </button>
  </div>

  <ul id="todo-list" class="todo-list">
    <!-- Tasks dynamically rendered here -->
  </ul>

  <footer class="app-footer">
    <span id="items-left">0 items left</span>
    <span class="storage-hint" id="storage-status"><i class="fas fa-database"></i> Storage Ready</span>
  </footer>
</div>

B Step 2: Styling & Responsive CSS

Modern CSS flexbox and grid layouts are used to make the UI look polished, centered, and responsive across desktop, tablet, and mobile screens.

CSS (style.css)
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
  color: #1e293b;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  overflow-x: hidden;
}
.todo-app {
  background: #ffffff;
  border-radius: 20px;
  width: 100%;
  max-width: 520px;
  box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.4);
  padding: 24px;
  border: 1px solid rgba(255, 255, 255, 0.2);
}
.app-header {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 20px;
}
.header-icon {
  width: 46px;
  height: 46px;
  background: rgba(16, 185, 129, 0.12);
  color: #10b981;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  flex-shrink: 0;
}
.app-header h1 {
  color: #0f172a;
  font-size: 1.5rem;
  font-weight: 800;
  line-height: 1.1;
}
.subtitle {
  color: #64748b;
  font-size: 0.85rem;
  margin-top: 3px;
}
.todo-form .input-group {
  display: flex;
  gap: 8px;
  margin-bottom: 18px;
}
#todo-input {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  border: 1.5px solid #cbd5e1;
  border-radius: 12px;
  font-size: 0.92rem;
  outline: none;
  transition: all 0.2s;
  background: #f8fafc;
}
#todo-input:focus {
  background: #ffffff;
  border-color: #10b981;
  box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.15);
}
#todo-priority {
  padding: 0 10px;
  border: 1.5px solid #cbd5e1;
  border-radius: 12px;
  background: #f8fafc;
  color: #334155;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  outline: none;
  flex-shrink: 0;
  transition: border-color 0.2s;
}
#todo-priority:focus {
  border-color: #10b981;
}
.btn-primary {
  background: #10b981;
  color: #ffffff;
  border: none;
  padding: 11px 18px;
  border-radius: 12px;
  font-weight: 700;
  font-size: 0.92rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-shrink: 0;
  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
  transition: all 0.2s;
}
.btn-primary:hover {
  background: #059669;
  transform: translateY(-1px);
}
.btn-primary:active {
  transform: translateY(0);
}
.controls-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #f1f5f9;
  flex-wrap: wrap;
  gap: 10px;
}
.filter-tabs {
  display: flex;
  background: #f1f5f9;
  padding: 4px;
  border-radius: 10px;
  gap: 4px;
}
.tab-btn {
  background: transparent;
  border: none;
  padding: 6px 12px;
  font-size: 0.82rem;
  font-weight: 700;
  color: #64748b;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
}
.tab-btn.active {
  background: #ffffff;
  color: #0f172a;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.btn-clear {
  background: transparent;
  border: none;
  color: #ef4444;
  font-size: 0.82rem;
  cursor: pointer;
  font-weight: 600;
  transition: color 0.2s;
  padding: 4px 6px;
}
.btn-clear:hover {
  color: #b91c1c;
  text-decoration: underline;
}
.todo-list {
  list-style: none;
  max-height: 320px;
  overflow-y: auto;
  margin-bottom: 16px;
  padding-right: 2px;
}
.todo-item {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  margin-bottom: 8px;
  transition: all 0.25s ease;
  animation: slideDown 0.25s ease;
  gap: 8px;
}
@keyframes slideDown {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
.todo-item:hover {
  background: #f1f5f9;
  border-color: #cbd5e1;
}
.todo-item.completed {
  background: #f8fafc;
  opacity: 0.65;
}
.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #94a3b8;
}
.checkbox-custom {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  cursor: pointer;
  accent-color: #10b981;
  flex-shrink: 0;
}
.todo-text {
  flex: 1;
  min-width: 0;
  font-size: 0.92rem;
  font-weight: 500;
  color: #1e293b;
  word-break: break-word;
  overflow-wrap: break-word;
}
.priority-tag {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 6px;
  flex-shrink: 0;
  letter-spacing: 0.4px;
}
.priority-high { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.priority-normal { background: #e0f2fe; color: #0284c7; border: 1px solid #bae6fd; }
.priority-low { background: #f1f5f9; color: #64748b; border: 1px solid #e2e8f0; }
.delete-btn {
  background: transparent;
  border: none;
  color: #94a3b8;
  font-size: 0.95rem;
  cursor: pointer;
  transition: color 0.2s;
  padding: 6px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.delete-btn:hover {
  color: #ef4444;
}
.empty-state {
  text-align: center;
  padding: 32px 10px;
  color: #94a3b8;
  font-size: 0.9rem;
}
.app-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.8rem;
  color: #64748b;
  font-weight: 500;
  border-top: 1px solid #f1f5f9;
  padding-top: 12px;
  flex-wrap: wrap;
  gap: 6px;
}
.storage-hint {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: #10b981;
  font-size: 0.76rem;
  font-weight: 600;
}

/* Mobile Responsiveness */
@media (max-width: 520px) {
  body {
    padding: 10px 8px;
    align-items: flex-start;
  }
  .todo-app {
    padding: 16px 12px;
    border-radius: 16px;
  }
  .app-header {
    gap: 10px;
    margin-bottom: 14px;
  }
  .header-icon {
    width: 38px;
    height: 38px;
    font-size: 1.1rem;
    border-radius: 10px;
  }
  .app-header h1 {
    font-size: 1.3rem;
  }
  .subtitle {
    font-size: 0.78rem;
  }
  .todo-form .input-group {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 8px;
  }
  #todo-input {
    grid-column: 1 / -1;
    width: 100%;
    padding: 10px 12px;
    font-size: 0.88rem;
  }
  #todo-priority {
    height: 38px;
    font-size: 0.82rem;
  }
  .btn-primary {
    height: 38px;
    padding: 0 14px;
    font-size: 0.85rem;
  }
  .controls-bar {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    margin-bottom: 12px;
  }
  .filter-tabs {
    width: 100%;
    display: flex;
  }
  .tab-btn {
    flex: 1;
    text-align: center;
    padding: 6px 4px;
    font-size: 0.76rem;
  }
  .btn-clear {
    width: 100%;
    text-align: center;
    padding: 4px;
    font-size: 0.78rem;
  }
  .todo-item {
    padding: 8px 10px;
    gap: 6px;
  }
  .todo-text {
    font-size: 0.86rem;
  }
  .priority-tag {
    font-size: 0.65rem;
    padding: 2px 5px;
  }
  .delete-btn {
    padding: 4px;
    font-size: 0.88rem;
  }
  .app-footer {
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-align: center;
  }
}

C Step 3: JavaScript Logic & Event Handling

Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:

JavaScript (script.js)
// Storage Safe Helper
const STORAGE_KEY = 'taskflow_todos_v2';

function loadTodos() {
  try {
    const data = localStorage.getItem(STORAGE_KEY);
    if (data) {
      const parsed = JSON.parse(data);
      if (Array.isArray(parsed) && parsed.length > 0) return parsed;
    }
  } catch (e) {
    console.warn('LocalStorage unavailable in sandbox, using memory state:', e);
  }
  return [
    { id: 1, text: 'Learn JavaScript basics & DOM manipulation', priority: 'high', completed: true },
    { id: 2, text: 'Build interactive beginner projects', priority: 'normal', completed: false },
    { id: 3, text: 'Master state management and event listeners', priority: 'low', completed: false }
  ];
}

function saveTodos(data) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  } catch (e) {}
}

let todos = loadTodos();
let currentFilter = 'all';

// DOM Elements
const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const prioritySelect = document.getElementById('todo-priority');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('todo-list');
const itemsLeft = document.getElementById('items-left');
const filterBtns = document.querySelectorAll('.tab-btn');
const clearCompletedBtn = document.getElementById('clear-completed-btn');

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function renderTodos() {
  list.innerHTML = '';

  const filtered = todos.filter(t => {
    if (currentFilter === 'active') return !t.completed;
    if (currentFilter === 'completed') return t.completed;
    return true;
  });

  if (filtered.length === 0) {
    list.innerHTML = `
      <div class="empty-state">
        <i class="far fa-clipboard" style="font-size:2.2rem;margin-bottom:8px;display:block;color:#cbd5e1"></i>
        No tasks found in this filter.
      </div>
    `;
  } else {
    filtered.forEach(todo => {
      const li = document.createElement('li');
      li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
      li.dataset.id = todo.id;

      li.innerHTML = `
        <input type="checkbox" class="checkbox-custom" ${todo.completed ? 'checked' : ''} aria-label="Mark task completed">
        <span class="todo-text">${escapeHtml(todo.text)}</span>
        <span class="priority-tag priority-${todo.priority}">${todo.priority}</span>
        <button type="button" class="delete-btn" title="Delete Task"><i class="fas fa-trash-alt"></i></button>
      `;

      list.appendChild(li);
    });
  }

  // Update active counter
  const activeCount = todos.filter(t => !t.completed).length;
  itemsLeft.textContent = `${activeCount} item${activeCount === 1 ? '' : 's'} left`;
}

function addTodo() {
  const text = input.value.trim();
  if (!text) {
    input.focus();
    return;
  }

  const newTodo = {
    id: Date.now(),
    text: text,
    priority: prioritySelect.value || 'normal',
    completed: false
  };

  todos.unshift(newTodo);
  saveTodos(todos);
  renderTodos();
  input.value = '';
  input.focus();
}

// Add task on Form Submit
form.addEventListener('submit', (e) => {
  e.preventDefault();
  addTodo();
});

// Add task on direct button click
addBtn.addEventListener('click', (e) => {
  e.preventDefault();
  addTodo();
});

// Add task on Enter key inside input
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault();
    addTodo();
  }
});

// Toggle complete & delete using event delegation
list.addEventListener('click', (e) => {
  const item = e.target.closest('.todo-item');
  if (!item) return;
  const id = Number(item.dataset.id);

  if (e.target.classList.contains('checkbox-custom')) {
    todos = todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
    saveTodos(todos);
    renderTodos();
  } else if (e.target.closest('.delete-btn')) {
    todos = todos.filter(t => t.id !== id);
    saveTodos(todos);
    renderTodos();
  }
});

// Filter Tabs
filterBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    filterBtns.forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    currentFilter = btn.dataset.filter;
    renderTodos();
  });
});

// Clear Completed
clearCompletedBtn.addEventListener('click', () => {
  todos = todos.filter(t => !t.completed);
  saveTodos(todos);
  renderTodos();
});

// Initial render
renderTodos();

3. Core JavaScript Concepts You Learned

Safe LocalStorage API

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Array Methods (map, filter, unshift)

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Event Delegation & Keyboard Triggers

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Dynamic HTML Template Rendering

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Form Submit Prevention

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

4. How to Run Locally on Your Computer

  1. Download the project ZIP: Click the Download Project (.ZIP) button at the top or in the live runner to save todo-list-project.zip.
  2. Extract the archive: Unzip the downloaded file. Inside you will find separate index.html, style.css, script.js, index-standalone.html, and a comprehensive README.md.
  3. Open in any browser: Simply double-click index.html (or index-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge.
  4. Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click index.html and select "Open with Live Server" to enjoy instant live reloading as you modify HTML, CSS, or JS!

Bonus Challenges for Learners

Ready to level up? Try adding these extra features on your own:

  • Add sound effects or audio feedback for user button clicks.
  • Add custom theme color customizer with CSS variables.
  • Export the results to PDF or copy as a formatted text summary.

Other Beginner Projects You Might Like

Beginner30 mins
Productivity & UI

Personal Expense & Budget Tracker

Track daily income and expenses with real-time balance calculations, transaction histories, deletion, and local storage support.

Array.prototype.reduce()localStorage PersistenceForm Validation+2
Beginner20 mins
Productivity & UI

Markdown & Rich Text Live Previewer

Split-screen live Markdown to HTML parser with word count, character count, and estimated reading time calculator.

Regex Markdown ParsingString Metrics (Words, Chars)Live Input Events+1