Hey fellow developers! 👋
Today I'm excited to share a project I've been working on - a minimalist to-do list application with some surprisingly powerful features under the hood. If you've ever wanted to build a practical, user-friendly web app that actually helps people organize their lives, this walkthrough is for you!
We'll create a clean, modern to-do list that includes drag-and-drop prioritization, local storage, dark mode, and more - all with vanilla JavaScript. No frameworks required!
You can check out the live demo here.
Features Overview 🔍
Here's what makes our to-do app special:
- Intuitive drag-and-drop interface for reordering tasks
- Persistent storage that saves your tasks between sessions
- Dark/light theme toggle for comfortable viewing any time of day
- Visual progress tracking to keep you motivated
- Task categorization with color-coding
- Due date assignment with overdue indicators
- Collapsible completed tasks section
- Handy keyboard shortcuts for power users
- Responsive design that works everywhere
- Undo functionality (because we all make mistakes!)
Let's dive into how it's built!
The Structure: HTML 🏗️
We'll start with the HTML structure that forms the foundation of our app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="icon-192.png">
<meta name="theme-color" content="#4caf50">
<meta name="description" content="A minimalist to-do list with advanced features">
<title>Minimalist To-Do List</title>
<link rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.json">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
</head>
<body>
<div class="app-container">
<header class="sticky-header">
<h1>Tasks</h1>
<div class="header-actions">
<button id="toggle-completed" class="action-btn">↓</button>
<button id="theme-toggle" class="theme-btn">☀️</button>
<div class="progress-bar">
<span id="progress-text">0% Complete</span>
<div id="progress-fill"></div>
</div>
</div>
</header>
<main>
<ul id="task-list" class="task-list"></ul>
<div id="completed-section" class="completed-section">
<h2>Completed</h2>
<ul id="completed-list" class="task-list"></ul>
</div>
</main>
<footer class="sticky-footer">
<div class="fab" id="add-task-btn">+</div>
<input type="text" id="task-input" placeholder="Add a new task..." autocomplete="off">
<input type="date" id="due-date-input" placeholder="Due Date">
<select id="category-select">
<option value="personal">Personal</option>
<option value="work">Work</option>
<option value="shopping">Shopping</option>
</select>
</footer>
<div id="undo-snackbar" class="snackbar">Task deleted. <span>Undo</span></div>
<div class="keys-guide">
<span>Shortcuts: </span>
<span>n: New | Esc: Exit | a: Add | d: Delete | c: Complete | t: Toggle Completed | m: Mode</span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
The HTML is organized into a few key sections:
- A sticky header containing the app title, theme toggle, and progress tracker
- The main task lists (active and completed)
- A sticky footer with inputs for adding new tasks
- An undo snackbar that appears when tasks are deleted
- A keyboard shortcuts guide for quick reference
I've used semantic HTML where possible, which helps with accessibility and gives our document a logical structure.
The Look: CSS Styling 🎨
The styling focuses on creating a clean, distraction-free interface:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background: #f0f2f5;
color: #333;
transition: all 0.3s ease;
}
body.dark {
background: #1a1a1a;
color: #e0e0e0;
}
.app-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
}
.sticky-header {
position: sticky;
top: 0;
background: #fff;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 10;
}
body.dark .sticky-header {
background: #2a2a2a;
}
h1 {
font-size: 24px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.theme-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
.progress-bar {
width: 100px;
height: 8px;
background: #ddd;
border-radius: 4px;
position: relative;
overflow: hidden;
}
#progress-fill {
height: 100%;
width: 0;
background: #4caf50;
transition: width 0.3s ease;
}
#progress-text {
font-size: 12px;
position: absolute;
top: -20px;
left: 0;
width: 100%;
text-align: center;
}
.task-list {
list-style: none;
margin: 20px 0;
}
.task-item {
background: #fff;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 10px;
cursor: move;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
}
body.dark .task-item {
background: #2a2a2a;
}
.task-item.dragging {
opacity: 0.5;
transform: scale(0.98);
}
.task-item.completed .task-text {
text-decoration: line-through;
color: #888;
}
.task-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 5px;
border-radius: 8px 0 0 8px;
}
.task-item.personal::before { background: #4caf50; }
.task-item.work::before { background: #2196f3; }
.task-item.shopping::before { background: #ff9800; }
.checkbox {
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox.checked {
background: #4caf50;
border-color: #4caf50;
}
.checkbox.checked::after {
content: '✔';
color: #fff;
font-size: 12px;
}
.task-text {
flex-grow: 1;
border: none;
background: none;
font-size: 16px;
outline: none;
color: inherit;
}
.due-date {
font-size: 12px;
color: #888;
margin-left: 10px;
}
.due-date.overdue {
color: #ff4444;
font-weight: 600;
}
.delete-btn {
background: none;
border: none;
color: #ff4444;
font-size: 18px;
cursor: pointer;
}
.sticky-footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 15px;
border-radius: 10px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: 40px 1fr 100px 100px;
gap: 10px;
}
body.dark .sticky-footer {
background: #2a2a2a;
}
.fab {
width: 40px;
height: 40px;
background: #4caf50;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
#task-input {
flex-grow: 1;
border: none;
background: #f5f5f5;
padding: 10px;
border-radius: 5px;
font-size: 16px;
outline: none;
}
body.dark #task-input {
background: #3a3a3a;
color: #e0e0e0;
}
#due-date-input, #category-select {
padding: 10px;
border: none;
background: #f5f5f5;
border-radius: 5px;
font-size: 14px;
outline: none;
}
body.dark #due-date-input, body.dark #category-select {
background: #3a3a3a;
color: #e0e0e0;
}
.completed-section {
margin-top: 20px;
display: none;
}
.completed-section.active {
display: block;
}
.completed-section h2 {
font-size: 18px;
margin-bottom: 10px;
color: #888;
}
.snackbar {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 10px 20px;
border-radius: 5px;
display: none;
z-index: 20;
}
.snackbar span {
color: #4caf50;
cursor: pointer;
margin-left: 10px;
}
.keys-guide {
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.8);
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
color: #666;
opacity: 0.7;
transition: opacity 0.3s ease;
z-index: 10;
}
body.dark .keys-guide {
background: rgba(42, 42, 42, 0.8);
color: #aaa;
}
.keys-guide:hover {
opacity: 1;
}
.keys-guide span:first-child {
font-weight: 600;
margin-right: 5px;
}
.action-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
}
@media (max-width: 600px) {
.sticky-footer {
grid-template-columns: 40px 1fr;
grid-template-rows: auto auto;
}
#due-date-input, #category-select {
grid-column: span 2;
}
}
Some styling highlights:
- Custom checkboxes that provide satisfying visual feedback
- Color-coded task categories for quick visual scanning
- A progress bar that updates in real-time
- Smooth transitions for all interactive elements
- Dark mode support with a simple class toggle
- Responsive layouts using flexbox and CSS grid
- Subtle shadows and rounded corners for a modern feel
The CSS is organized by component, making it easy to find and modify specific parts of the interface.
The Brains: JavaScript Functionality ⚙️
Now for the exciting part - bringing our app to life with JavaScript:
const taskList = document.getElementById('task-list');
const completedList = document.getElementById('completed-list');
const taskInput = document.getElementById('task-input');
const dueDateInput = document.getElementById('due-date-input');
const categorySelect = document.getElementById('category-select');
const addTaskBtn = document.getElementById('add-task-btn');
const themeToggle = document.getElementById('theme-toggle');
const toggleCompleted = document.getElementById('toggle-completed');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const undoSnackbar = document.getElementById('undo-snackbar');
const completedSection = document.getElementById('completed-section');
let tasks = JSON.parse(localStorage.getItem('tasks')) || [];
let deletedTasks = [];
function saveTasks() {
localStorage.setItem('tasks', JSON.stringify(tasks));
updateProgress();
}
function updateProgress() {
const total = tasks.length;
const completed = tasks.filter(task => task.completed).length;
const percentage = total === 0 ? 0 : (completed / total) * 100;
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${Math.round(percentage)}% Complete`;
}
function renderTasks() {
taskList.innerHTML = '';
completedList.innerHTML = '';
const today = new Date().toISOString().split('T')[0];
tasks.forEach((task, index) => {
const li = document.createElement('li');
li.className = `task-item ${task.completed ? 'completed' : ''} ${task.category}`;
li.draggable = true;
li.dataset.index = index;
const isOverdue = task.dueDate && task.dueDate < today && !task.completed;
li.innerHTML = `
<div class="checkbox ${task.completed ? 'checked' : ''}"></div>
<input type="text" class="task-text" value="${task.text}">
${task.dueDate ? `<span class="due-date ${isOverdue ? 'overdue' : ''}">${task.dueDate}</span>` : ''}
<button class="delete-btn">✕</button>
`;
if (task.completed) {
completedList.appendChild(li);
} else {
taskList.appendChild(li);
}
});
addDragListeners(taskList);
addDragListeners(completedList);
updateProgress();
}
function addTask() {
const text = taskInput.value.trim();
if (text) {
tasks.push({
text,
completed: false,
dueDate: dueDateInput.value || null,
category: categorySelect.value
});
taskInput.value = '';
dueDateInput.value = '';
saveTasks();
renderTasks();
}
}
function deleteTask(index) {
const deletedTask = tasks.splice(index, 1)[0];
deletedTasks.push(deletedTask);
saveTasks();
renderTasks();
showUndoSnackbar();
}
function showUndoSnackbar() {
if (deletedTasks.length > 0) {
undoSnackbar.style.display = 'block';
setTimeout(() => {
if (undoSnackbar.style.display === 'block') {
undoSnackbar.style.display = 'none';
}
}, 3000);
}
}
function undoDelete() {
if (deletedTasks.length > 0) {
const lastDeleted = deletedTasks.pop();
tasks.push(lastDeleted);
saveTasks();
renderTasks();
if (deletedTasks.length > 0) {
showUndoSnackbar();
} else {
undoSnackbar.style.display = 'none';
}
}
}
function addDragListeners(list) {
const items = list.querySelectorAll('.task-item');
items.forEach(item => {
item.addEventListener('dragstart', () => item.classList.add('dragging'));
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
const newIndex = [...list.children].indexOf(item);
const oldIndex = parseInt(item.dataset.index);
if (newIndex !== oldIndex) {
const [movedTask] = tasks.splice(oldIndex, 1);
tasks.splice(newIndex, 0, movedTask);
saveTasks();
}
});
});
list.addEventListener('dragover', e => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
const afterElement = getDragAfterElement(list, e.clientY);
if (afterElement == null) {
list.appendChild(dragging);
} else {
list.insertBefore(dragging, afterElement);
}
});
}
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.task-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
}
return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
[taskList, completedList].forEach(list => {
list.addEventListener('click', e => {
const index = e.target.closest('.task-item')?.dataset.index;
if (!index) return;
if (e.target.classList.contains('checkbox')) {
tasks[index].completed = !tasks[index].completed;
saveTasks();
renderTasks();
} else if (e.target.classList.contains('delete-btn')) {
deleteTask(index);
}
});
list.addEventListener('input', e => {
const index = e.target.closest('.task-item')?.dataset.index;
if (e.target.classList.contains('task-text')) {
tasks[index].text = e.target.value;
saveTasks();
}
});
});
addTaskBtn.addEventListener('click', addTask);
taskInput.addEventListener('keypress', e => e.key === 'Enter' && addTask());
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark');
themeToggle.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
});
toggleCompleted.addEventListener('click', () => {
completedSection.classList.toggle('active');
toggleCompleted.textContent = completedSection.classList.contains('active') ? '↑' : '↓';
});
undoSnackbar.querySelector('span').addEventListener('click', undoDelete);
document.addEventListener('keydown', e => {
if (e.target.matches('input')) {
if (e.key === 'Escape') taskInput.blur();
return;
}
switch (e.key) {
case 'n':
taskInput.focus();
break;
case 'a':
addTask();
break;
case 'd':
if (tasks.length) deleteTask(tasks.length - 1);
break;
case 'c':
if (tasks.length) {
tasks[tasks.length - 1].completed = !tasks[tasks.length - 1].completed;
saveTasks();
renderTasks();
}
break;
case 't':
completedSection.classList.toggle('active');
toggleCompleted.textContent = completedSection.classList.contains('active') ? '↑' : '↓';
break;
case 'm':
document.body.classList.toggle('dark');
themeToggle.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
break;
case 'u':
undoDelete();
break;
}
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(() => console.log('Service Worker Registered'))
.catch(err => console.log('Service Worker Error:', err));
}
document.querySelector('.keys-guide span:last-child').textContent =
'n: New | Esc: Exit | a: Add | d: Delete | c: Complete | t: Toggle Completed | m: Mode | u: Undo';
renderTasks();
Let's explore the key functionality pieces:
Task Data & Storage 💾
We use the browser's localStorage API to persist tasks between sessions:
let tasks = JSON.parse(localStorage.getItem('tasks')) || [];
function saveTasks() {
localStorage.setItem('tasks', JSON.stringify(tasks));
updateProgress();
}
This simple yet powerful approach means users never lose their tasks when they close the browser - a crucial feature for any productivity app!
Task Rendering Engine 🔄
The app dynamically renders tasks based on their properties:
function renderTasks() {
taskList.innerHTML = '';
completedList.innerHTML = '';
const today = new Date().toISOString().split('T')[0];
tasks.forEach((task, index) => {
// Create and populate task elements
// ...
});
addDragListeners(taskList);
addDragListeners(completedList);
updateProgress();
}
Each task is created as a DOM element with the appropriate classes and attributes, then inserted into either the active or completed list.
Drag-and-Drop Magic ✨
The most satisfying feature is the ability to reorder tasks by dragging:
function addDragListeners(list) {
}
function getDragAfterElement(container, y) {
}
This implementation uses the native HTML5 Drag and Drop API, providing a smooth, intuitive way to prioritize tasks without any external libraries.
Progress Tracking 📊
The progress bar gives you a visual sense of accomplishment:
function updateProgress() {
const total = tasks.length;
const completed = tasks.filter(task => task.completed).length;
const percentage = total === 0 ? 0 : (completed / total) * 100;
progressFill.style.width = `${percentage}%`;
progressText.textContent = `${Math.round(percentage)}% Complete`;
}
As you complete tasks, the progress percentage updates automatically, giving that dopamine hit that keeps you productive!
Undo Functionality 🔙
Everyone makes mistakes, so we implemented a simple but effective undo system:
let deletedTasks = [];
function deleteTask(index) {
const deletedTask = tasks.splice(index, 1)[0];
deletedTasks.push(deletedTask);
saveTasks();
renderTasks();
showUndoSnackbar();
}
function undoDelete() {
if (deletedTasks.length > 0) {
const lastDeleted = deletedTasks.pop();
tasks.push(lastDeleted);
saveTasks();
renderTasks();
// ...
}
}
When a task is deleted, it's temporarily stored in a separate array. The undo function retrieves the most recently deleted task and restores it to the list.
Keyboard Shortcuts ⌨️
For the productivity-obsessed (like me!), keyboard shortcuts make everything faster:
document.addEventListener('keydown', e => {
if (e.target.matches('input')) {
if (e.key === 'Escape') taskInput.blur();
return;
}
switch (e.key) {
case 'n':
taskInput.focus();
break;
// Other shortcuts
// ...
}
});
These shortcuts make the app much more efficient once you get used to them. No more reaching for the mouse to add a new task!
Offline Support 📶
A simple service worker provides offline capabilities:
const CACHE_NAME = 'todo-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/script.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
This means the app works even when you're not connected to the internet - perfect for jotting down ideas anywhere.
Technical Deep Dive 🧠
Let's explore some of the more interesting technical aspects:
Event Delegation Pattern
Rather than attaching event listeners to individual task elements (which would be inefficient), we use event delegation:
// Add event delegation example here
This pattern attaches a single event listener to a parent element and uses e.target
to determine which child element was clicked. It's more efficient and handles dynamically created elements automatically.
Drag Position Algorithm
The algorithm for determining where to place a dragged task is particularly clever:
// Add drag position algorithm here
This function calculates the optimal insertion point based on the mouse position relative to other tasks, creating a smooth and intuitive user experience.
Dynamic Theme Switching
The theme toggle uses a simple but effective approach:
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark');
themeToggle.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
});
By toggling a class on the body element, we can switch all colors throughout the application with just a few lines of CSS.
Lessons Learned 📝
Building this app taught me several important lessons:
Start simple, add complexity gradually - I began with basic task adding/removing and only added drag-and-drop after the core functionality was solid.
User experience matters - Small details like smooth animations and keyboard shortcuts make a huge difference in how the app feels.
Local storage is powerful - You can create surprisingly capable apps without a backend by leveraging browser storage.
Test on different devices - What works on your development machine might not work as expected on mobile devices.
Performance considerations - Even for a simple app, thinking about efficiency (like using event delegation) pays dividends.
Try It Yourself! 🚀
The best way to learn is by doing. Here are some challenges if you want to extend this project:
- Add task filtering by category
- Implement search functionality
- Create nested subtasks
- Add recurring tasks
- Enable task sharing via URL
- Implement data export/import
- Add custom categories with user-defined colors
Conclusion 🎯
Building a to-do app might seem basic at first glance, but it's a perfect project to practice modern web development techniques. This implementation demonstrates how vanilla JavaScript, HTML, and CSS can create a polished, functional application without frameworks or libraries.
The focus on user experience - from the satisfying drag-and-drop to the visual progress tracking - elevates this from a simple demo to something people might actually want to use daily.
What would you add to this to-do app? Have you built something similar? I'd love to hear your thoughts and suggestions in the comments!
Happy coding! 💻✨
You can find the live demo at https://playground.learncomputer.in/minimalist-to-do-list/.