Create a Delightful Drag-and-Drop To-Do App: A Web Dev Journey
Learn Computer Academy

Learn Computer Academy @learncomputer

About: Website Design and Development Training Institute in Habra We provide the best hands-on training in Graphics Design, Website Design and Development.

Location:
Habra, India
Joined:
Mar 7, 2025

Create a Delightful Drag-and-Drop To-Do App: A Web Dev Journey

Publish Date: Apr 2
0 0

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>
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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) {

}
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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();
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        // ...
    }
});
Enter fullscreen mode Exit fullscreen mode

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))
    );
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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') ? '☀️' : '🌙';
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Start simple, add complexity gradually - I began with basic task adding/removing and only added drag-and-drop after the core functionality was solid.

  2. User experience matters - Small details like smooth animations and keyboard shortcuts make a huge difference in how the app feels.

  3. Local storage is powerful - You can create surprisingly capable apps without a backend by leveraging browser storage.

  4. Test on different devices - What works on your development machine might not work as expected on mobile devices.

  5. 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/.

Comments 0 total

    Add comment