More Dashboards Snippets
Todo Widget — Free HTML CSS JS Task List Snippet
Todo Widget · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Todo Widget — Task List with Progress Bar, Filter Tabs, Add Task & Clear Done

A todo widget is one of the most practical dashboard components you can build — it gives users a lightweight task management view without navigating to a separate app. This snippet provides a complete task list widget: an add-task input, checkbox toggles, a hover-reveal delete button, a gradient progress bar, All/Active/Done filter tabs, a remaining count footer, and a clear-done button — all in plain HTML, CSS, and vanilla JavaScript with no library.
How tasks are stored and rendered
Tasks are stored as an array of objects: { id, text, done }. The render() function completely re-renders the list on every change. While not the most efficient approach for very large lists, it is the simplest and most readable pattern for a small dashboard widget — no virtual DOM, no diffing, just innerHTML.
The progress bar
A gradient progress bar (indigo → green via linear-gradient) shows completion as a percentage: done/total * 100%. The bar fill uses transition: width 0.4s ease for smooth animation on each task toggle. The summary text below the heading updates simultaneously.
The checkbox appearance
The native checkbox is hidden (appearance: none) and replaced with a custom 18px square using the ::after pseudo-element. The unchecked state shows a grey border. When checked, background: #6366f1 fills the box and the ::after shows a white ✓ checkmark. The CSS-only custom checkbox avoids any SVG or image requirement.
Filter tabs and hover delete
The All/Active/Done tabs add a .hidden class to non-matching items using the currentFilter state. The delete button uses opacity: 0 by default and opacity: 1 on .task-item:hover — a hover-reveal pattern that keeps the list clean until the user wants to delete.
Adding tasks
The add-task input fires addTask() on the + button click and on Enter key (addOnEnter). The task is pushed to the array and render() is called. The input clears on add. Empty inputs are ignored.
Persistence
By default, tasks reset on page reload. To persist: JSON.stringify the tasks array into localStorage on every render(), and JSON.parse it on load. Add const saved = localStorage.getItem("tasks"); if (saved) tasks = JSON.parse(saved); at the top of the script. Keep nextId as Math.max(...tasks.map(t => t.id)) + 1 after parsing so new tasks get unique IDs even after a page reload.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Ask an AI coding assistant like Claude to explain why render() fully clears and rebuilds the task list's innerHTML on every single change — toggling one checkbox, deleting one item — instead of patching just the affected row, and what tradeoff that full-rebuild simplicity makes as the list grows to hundreds of tasks. It's also worth asking specifically how nextId should be recomputed after loading persisted tasks from localStorage, since the current code assumes a fresh in-memory counter. For extending it, ask for drag-to-reorder tasks, due dates with an overdue visual state, or nested subtasks that roll up into the parent's completion percentage. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a todo list widget in plain HTML, CSS, and JavaScript with add, complete, delete, filtering, and a progress bar — state stored as a plain array of objects, no framework.
Requirements:
- Store tasks as an array of objects, each with a unique numeric id, text, and a done boolean; every user action (add, toggle, delete, clear-done) must mutate this array and then call a single render function that fully redraws the task list from it.
- An add-task input that creates a new task on both pressing Enter and clicking a separate add button, trims whitespace, ignores empty submissions, and clears itself after a successful add.
- Each rendered task row must include a custom checkbox built by hiding the native checkbox's default appearance and drawing a checked state with a pseudo-element checkmark, a text label that gets a line-through style when the task is done, and a delete button that is invisible until the row is hovered.
- Compute and display, on every render, a gradient progress bar whose width reflects the completed-to-total task ratio, a summary line like "3 of 7 completed", and a "remaining" count.
- Implement All/Active/Done filter tabs that hide non-matching rows by toggling a class rather than removing them from the underlying array, so filtering never mutates the actual task data.
- A "clear done" action that removes every completed task from the array in one operation, and confirm that toggling, deleting, and filtering all continue to work correctly against whatever tasks remain.Step by step
How to Use
- 1Add a taskType in the input field and press Enter or click the + button. The task appears at the bottom of the list and the remaining count and progress bar update immediately.
- 2Complete a taskClick the checkbox on any task item. The text gets a line-through style, the item moves to the "done" state, and the progress bar advances toward 100%.
- 3Delete a taskHover over any task row to reveal the × delete button on the right. Click it to remove the task permanently from the list.
- 4Filter the listClick All to show every task, Active to show only incomplete tasks, or Done to show only completed tasks. The filter is applied instantly without re-fetching.
- 5Clear completed tasksClick "Clear done" in the footer to remove all completed tasks at once. Useful for keeping the list clean after a daily review.
- 6Persist tasks across page loadsAt the end of render(), add localStorage.setItem("tasks", JSON.stringify(tasks)). At the top of the script before render(), add: const saved = localStorage.getItem("tasks"); if (saved) tasks = JSON.parse(saved);
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Add two changes. At the top of the script, after defining tasks: const saved = localStorage.getItem("ui_tasks"); if (saved) { try { tasks = JSON.parse(saved); nextId = Math.max(...tasks.map(t => t.id)) + 1; } catch(e) {} }. At the end of the render() function, add: localStorage.setItem("ui_tasks", JSON.stringify(tasks)). This saves on every render and restores on page load. The nextId calculation ensures new tasks get unique IDs even after a reload.
The checkbox has appearance: none which removes all native browser styling. A custom 18px square is drawn using border and border-radius. The :checked state adds background: #6366f1. The ::after pseudo-element shows the checkmark using content: "✓" with white colour and bold weight. Both ::after styles are always present in CSS — the content only becomes visible when the checkbox state changes to :checked.
On mount: fetch("/api/tasks").then(r => r.json()).then(data => { tasks = data; render(); }). In toggle(): fetch("/api/tasks/"+id, { method: "PATCH", body: JSON.stringify({done: task.done}) }). In remove(): fetch("/api/tasks/"+id, { method: "DELETE" }). In addTask(): const res = await fetch("/api/tasks", { method: "POST", body: JSON.stringify({text}) }); const saved = await res.json(); tasks.push(saved). The local render() calls remain unchanged.
Click "JSX" to download. Manage tasks as useState<Task[]>(initialTasks). Each operation becomes a state update: toggle = setTasks(prev => prev.map(t => t.id === id ? {...t, done: !t.done} : t)). remove = setTasks(prev => prev.filter(t => t.id !== id)). addTask = setTasks(prev => [...prev, {id: Date.now(), text, done: false}]). Derive progressPct, remaining, and filteredTasks with useMemo. For persistence, use useEffect to write localStorage on tasks change.