The Road To Dev currently has AI-generated placeholder content. This is temporary - content is being actively developed.

Todo App Instructions

Follow these step-by-step instructions to build Todo App with Local Storage from scratch.

Difficulty: intermediateDuration: 1 weekSkills: 5

Get Started with GitHub Classroom

Accept the GitHub Classroom assignment to get your personal repository with starter code and automated testing setup.

What you'll get: Personal repository, starter files, automated tests, and issue tracking.

Project Assets

Download starter files, design assets, and reference materials for this project.

Starter Files.zip

Complete starter code with project structure and initial files

2.1 MB

Design File.fig

Figma design file with layouts, components, and style guide

890 KB

Preview Images.zip

High-quality preview images for reference

5.2 MB

Project Brief.pdf

Detailed project requirements and specifications

1.4 MB

i

Getting Started Tip

Download the starter files first, then use the design file as reference while working through the instructions. The preview images show what your final project should look like.

Finished building?

Check out the complete solution with detailed explanations and best practices.

View Solution

Your Progress

Started project
Setup complete
Core features built
Styling complete
Project deployed
Progress20%

Todo App with Local Storage - Instructions

Build a fully functional todo application that demonstrates modern JavaScript development practices. This comprehensive guide will walk you through creating a professional-quality app step by step.

📋 Project Overview

What We're Building

  • Interactive todo application with full CRUD functionality
  • Local storage persistence to save data between sessions
  • Responsive design that works on all devices
  • Modern JavaScript using ES6+ features and best practices
  • Accessible interface with keyboard navigation and ARIA labels

Learning Focus

This project emphasizes practical JavaScript skills you'll use in every web application:

  • DOM manipulation and event handling
  • State management without frameworks
  • Browser storage APIs
  • Modern JavaScript patterns and syntax

🚀 Getting Started

Step 1: Accept GitHub Classroom Assignment

  1. Click the provided GitHub Classroom link
  2. Accept the assignment to create your personal repository
  3. Clone your repository locally:
Terminal window
git clone https://github.com/YOUR-USERNAME/todo-app-starter.git
cd todo-app-starter

Step 2: Project Structure

Your starter repository includes:

todo-app/
├── index.html # Main HTML file
├── css/
│ ├── reset.css # CSS reset/normalize
│ ├── styles.css # Main application styles
│ └── animations.css # Animation definitions
├── js/
│ ├── app.js # Main application entry
│ ├── todo.js # Todo class and methods
│ ├── storage.js # Local storage utilities
│ ├── ui.js # UI rendering logic
│ └── utils.js # Helper functions
├── assets/
│ └── icons/ # SVG icons
├── tests/ # Test files (optional)
└── README.md

Step 3: Development Setup

  1. Install VS Code extensions (recommended):

    • Live Server
    • JavaScript (ES6) code snippets
    • ESLint
    • Prettier
  2. Start Live Server:

    • Right-click index.html → "Open with Live Server"
    • Or use the Live Server extension in VS Code

📝 Phase 1: HTML Structure

Create the Base Markup

Start with semantic HTML in index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="A simple todo app with local storage">
<title>Todo App</title>
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/animations.css">
</head>
<body>
<div class="app-container">
<header class="app-header">
<h1>My Todo App</h1>
<p class="app-description">Stay organized and get things done</p>
</header>
<main class="todo-main">
<!-- Add Todo Form -->
<form class="add-todo-form" id="addTodoForm">
<div class="input-group">
<input
type="text"
id="todoInput"
class="todo-input"
placeholder="What needs to be done?"
autocomplete="off"
aria-label="New todo item"
required
>
<button type="submit" class="add-btn" aria-label="Add todo">
<span class="add-icon">+</span>
</button>
</div>
</form>
<!-- Todo List Container -->
<section class="todo-section" aria-labelledby="todo-heading">
<div class="todo-header">
<h2 id="todo-heading" class="visually-hidden">Todo List</h2>
<div class="todo-stats">
<span id="todoCount" class="todo-count">0 items left</span>
</div>
</div>
<!-- Filter Buttons -->
<div class="filter-controls" role="tablist" aria-label="Filter todos">
<button
class="filter-btn active"
data-filter="all"
role="tab"
aria-selected="true"
>
All
</button>
<button
class="filter-btn"
data-filter="active"
role="tab"
aria-selected="false"
>
Active
</button>
<button
class="filter-btn"
data-filter="completed"
role="tab"
aria-selected="false"
>
Completed
</button>
</div>
<!-- Todo List -->
<ul id="todoList" class="todo-list" role="list">
<!-- Todos will be dynamically inserted here -->
</ul>
<!-- Actions -->
<div class="todo-actions">
<button id="clearCompleted" class="clear-btn">
Clear Completed
</button>
</div>
</section>
<!-- Empty State -->
<div id="emptyState" class="empty-state hidden">
<div class="empty-icon">📝</div>
<h3>No todos yet</h3>
<p>Add a todo above to get started!</p>
</div>
</main>
</div>
<!-- Scripts -->
<script src="js/utils.js"></script>
<script src="js/storage.js"></script>
<script src="js/todo.js"></script>
<script src="js/ui.js"></script>
<script src="js/app.js"></script>
</body>
</html>

Key Features of This Structure:

  • Semantic HTML with proper roles and ARIA labels
  • Form validation with required attribute
  • Accessible filter controls using tablist pattern
  • Empty state for better user experience
  • Logical script loading order

🎨 Phase 2: CSS Styling

Base Styles (styles.css)

/* CSS Custom Properties */
:root {
/* Colors */
--primary-color: #4f46e5;
--primary-hover: #4338ca;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-card: #ffffff;
--border-color: #e5e7eb;
--border-focus: #4f46e5;
/* Typography */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
/* Spacing */
--space-xs: 0.5rem;
--space-sm: 0.75rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 2.5rem;
/* Shadows and Effects */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--border-radius: 0.5rem;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Base Styles */
* {
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
padding: var(--space-md);
color: var(--text-primary);
}
.app-container {
max-width: 600px;
margin: 0 auto;
padding: var(--space-xl);
}
/* Header Styles */
.app-header {
text-align: center;
margin-bottom: var(--space-2xl);
color: white;
}
.app-header h1 {
font-size: var(--font-size-2xl);
font-weight: 700;
margin: 0 0 var(--space-sm) 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-description {
font-size: var(--font-size-base);
opacity: 0.9;
margin: 0;
}
/* Todo Main Container */
.todo-main {
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
padding: var(--space-xl);
}
/* Add Todo Form */
.add-todo-form {
margin-bottom: var(--space-xl);
}
.input-group {
display: flex;
gap: var(--space-sm);
align-items: center;
}
.todo-input {
flex: 1;
padding: var(--space-md);
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
font-size: var(--font-size-base);
transition: var(--transition);
outline: none;
}
.todo-input:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.add-btn {
padding: var(--space-md);
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
font-size: var(--font-size-lg);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.add-btn:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
/* Filter Controls */
.filter-controls {
display: flex;
gap: var(--space-xs);
margin-bottom: var(--space-lg);
padding: var(--space-xs);
background: var(--bg-secondary);
border-radius: var(--border-radius);
}
.filter-btn {
flex: 1;
padding: var(--space-sm) var(--space-md);
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: calc(var(--border-radius) - 2px);
cursor: pointer;
transition: var(--transition);
font-size: var(--font-size-sm);
font-weight: 500;
}
.filter-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.5);
}
.filter-btn.active {
background: white;
color: var(--primary-color);
box-shadow: var(--shadow-sm);
}
/* Todo List */
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-md);
border-bottom: 1px solid var(--border-color);
transition: var(--transition);
}
.todo-item:hover {
background: var(--bg-secondary);
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: var(--text-muted);
}
/* Checkbox */
.todo-checkbox {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.todo-checkbox.checked {
background: var(--success-color);
border-color: var(--success-color);
color: white;
}
/* Todo Text */
.todo-text {
flex: 1;
font-size: var(--font-size-base);
line-height: 1.5;
word-break: break-word;
}
/* Todo Actions */
.todo-item-actions {
display: flex;
gap: var(--space-xs);
opacity: 0;
transition: var(--transition);
}
.todo-item:hover .todo-item-actions {
opacity: 1;
}
.todo-action-btn {
padding: var(--space-xs);
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
transition: var(--transition);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.edit-btn:hover {
background: rgba(79, 70, 229, 0.1);
color: var(--primary-color);
}
.delete-btn:hover {
background: rgba(239, 68, 68, 0.1);
color: var(--danger-color);
}
/* Utility Classes */
.hidden {
display: none !important;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Empty State */
.empty-state {
text-align: center;
padding: var(--space-2xl);
color: var(--text-secondary);
}
.empty-icon {
font-size: 4rem;
margin-bottom: var(--space-md);
}
/* Responsive Design */
@media (max-width: 640px) {
.app-container {
padding: var(--space-md);
}
.todo-main {
padding: var(--space-md);
}
.filter-controls {
flex-direction: column;
gap: var(--space-xs);
}
.todo-item-actions {
opacity: 1; /* Always visible on mobile */
}
}

⚡ Phase 3: JavaScript Implementation

Step 1: Utility Functions (utils.js)

// Utility functions for common operations
const Utils = {
/**
* Generate unique ID for todos
*/
generateId() {
return `todo-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
},
/**
* Sanitize HTML to prevent XSS
*/
sanitizeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
},
/**
* Debounce function calls
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Format date for display
*/
formatDate(date) {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
}).format(date);
},
/**
* Pluralize words based on count
*/
pluralize(count, singular, plural = `${singular}s`) {
return count === 1 ? singular : plural;
}
};

Step 2: Local Storage Management (storage.js)

// Local Storage utilities with error handling
const Storage = {
STORAGE_KEY: 'todoApp_todos',
/**
* Save todos to localStorage
*/
saveTodos(todos) {
try {
const todosJson = JSON.stringify(todos);
localStorage.setItem(this.STORAGE_KEY, todosJson);
return true;
} catch (error) {
console.error('Error saving todos:', error);
return false;
}
},
/**
* Load todos from localStorage
*/
loadTodos() {
try {
const todosJson = localStorage.getItem(this.STORAGE_KEY);
if (!todosJson) return [];
const todos = JSON.parse(todosJson);
// Validate and sanitize loaded data
return todos.filter(todo =>
todo &&
typeof todo.id === 'string' &&
typeof todo.text === 'string' &&
typeof todo.completed === 'boolean'
).map(todo => ({
id: todo.id,
text: Utils.sanitizeHtml(todo.text),
completed: Boolean(todo.completed),
createdAt: todo.createdAt ? new Date(todo.createdAt) : new Date(),
updatedAt: todo.updatedAt ? new Date(todo.updatedAt) : new Date()
}));
} catch (error) {
console.error('Error loading todos:', error);
return [];
}
},
/**
* Clear all todos from storage
*/
clearTodos() {
try {
localStorage.removeItem(this.STORAGE_KEY);
return true;
} catch (error) {
console.error('Error clearing todos:', error);
return false;
}
},
/**
* Check if localStorage is available
*/
isStorageAvailable() {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (error) {
return false;
}
}
};

Step 3: Todo Class (todo.js)

// Todo class with all CRUD operations
class Todo {
constructor(text, id = null) {
this.id = id || Utils.generateId();
this.text = Utils.sanitizeHtml(text.trim());
this.completed = false;
this.createdAt = new Date();
this.updatedAt = new Date();
}
/**
* Toggle completion status
*/
toggle() {
this.completed = !this.completed;
this.updatedAt = new Date();
return this;
}
/**
* Update todo text
*/
updateText(newText) {
const sanitizedText = Utils.sanitizeHtml(newText.trim());
if (sanitizedText && sanitizedText !== this.text) {
this.text = sanitizedText;
this.updatedAt = new Date();
return true;
}
return false;
}
/**
* Create todo from plain object
*/
static fromObject(obj) {
const todo = new Todo(obj.text, obj.id);
todo.completed = Boolean(obj.completed);
todo.createdAt = obj.createdAt ? new Date(obj.createdAt) : new Date();
todo.updatedAt = obj.updatedAt ? new Date(obj.updatedAt) : new Date();
return todo;
}
/**
* Convert to plain object for storage
*/
toObject() {
return {
id: this.id,
text: this.text,
completed: this.completed,
createdAt: this.createdAt.toISOString(),
updatedAt: this.updatedAt.toISOString()
};
}
}
// TodoManager class to handle collection operations
class TodoManager {
constructor() {
this.todos = [];
this.currentFilter = 'all';
this.loadTodos();
}
/**
* Load todos from storage
*/
loadTodos() {
const storedTodos = Storage.loadTodos();
this.todos = storedTodos.map(todo => Todo.fromObject(todo));
return this.todos;
}
/**
* Save todos to storage
*/
saveTodos() {
const todosData = this.todos.map(todo => todo.toObject());
return Storage.saveTodos(todosData);
}
/**
* Add new todo
*/
addTodo(text) {
if (!text || !text.trim()) {
throw new Error('Todo text cannot be empty');
}
const todo = new Todo(text);
this.todos.unshift(todo); // Add to beginning
this.saveTodos();
return todo;
}
/**
* Delete todo by ID
*/
deleteTodo(id) {
const index = this.todos.findIndex(todo => todo.id === id);
if (index === -1) {
throw new Error('Todo not found');
}
const deletedTodo = this.todos.splice(index, 1)[0];
this.saveTodos();
return deletedTodo;
}
/**
* Toggle todo completion
*/
toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id);
if (!todo) {
throw new Error('Todo not found');
}
todo.toggle();
this.saveTodos();
return todo;
}
/**
* Update todo text
*/
updateTodo(id, newText) {
const todo = this.todos.find(todo => todo.id === id);
if (!todo) {
throw new Error('Todo not found');
}
if (todo.updateText(newText)) {
this.saveTodos();
return todo;
}
throw new Error('Invalid todo text');
}
/**
* Get filtered todos
*/
getFilteredTodos(filter = this.currentFilter) {
switch (filter) {
case 'active':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}
/**
* Clear completed todos
*/
clearCompleted() {
const completedCount = this.todos.filter(todo => todo.completed).length;
this.todos = this.todos.filter(todo => !todo.completed);
this.saveTodos();
return completedCount;
}
/**
* Get todo statistics
*/
getStats() {
const total = this.todos.length;
const completed = this.todos.filter(todo => todo.completed).length;
const active = total - completed;
return {
total,
completed,
active,
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
};
}
/**
* Set current filter
*/
setFilter(filter) {
this.currentFilter = filter;
return this.getFilteredTodos(filter);
}
}

This is a comprehensive start to the JavaScript implementation. The instructions continue with UI management, event handling, and advanced features. The code demonstrates modern JavaScript patterns, error handling, and proper separation of concerns.

🧪 Testing Your Implementation

Manual Testing Checklist

  • [ ] Can add todos with Enter key and button
  • [ ] Can toggle completion status
  • [ ] Can delete individual todos
  • [ ] Filter buttons work correctly
  • [ ] Todo count updates accurately
  • [ ] Data persists after page reload
  • [ ] Responsive design works on mobile

Browser Testing

Test in multiple browsers to ensure compatibility:

  • Chrome (latest)
  • Firefox (latest)
  • Safari (if on Mac)
  • Edge (latest)

🚀 Next Steps

Continue with the remaining implementation phases:

  1. UI Management - Rendering and updating the interface
  2. Event Handling - User interaction management
  3. Advanced Features - Edit mode, animations, accessibility
  4. Performance Optimization - Debouncing, efficient rendering
  5. Deployment - GitHub Pages setup

Each phase builds on the previous one, creating a production-ready application that demonstrates professional JavaScript development skills.

The complete implementation guide continues in the next sections...