Todo App Instructions
Follow these step-by-step instructions to build Todo App with Local Storage from scratch.
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
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.
Quick Actions
Your Progress
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
- Click the provided GitHub Classroom link
- Accept the assignment to create your personal repository
- Clone your repository locally:
git clone https://github.com/YOUR-USERNAME/todo-app-starter.gitcd 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
-
Install VS Code extensions (recommended):
- Live Server
- JavaScript (ES6) code snippets
- ESLint
- Prettier
-
Start Live Server:
- Right-click
index.html
→ "Open with Live Server" - Or use the Live Server extension in VS Code
- Right-click
📝 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 operationsconst 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 handlingconst 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 operationsclass 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 operationsclass 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:
- UI Management - Rendering and updating the interface
- Event Handling - User interaction management
- Advanced Features - Edit mode, animations, accessibility
- Performance Optimization - Debouncing, efficient rendering
- 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...