Todo Widget — Free HTML CSS JS Task List Snippet

Todo Widget · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Tasks stored as array of objects: {id, text, done} — render() re-draws on every change
Gradient progress bar: linear-gradient indigo→green, transition:width 0.4s
Custom checkbox: appearance:none, ::after checkmark, fills on :checked
Hover-reveal delete button: opacity:0→1 on .task-item:hover
All/Active/Done filter tabs: .hidden class on non-matching items
Add on Enter key: onkeydown addOnEnter checks e.key === "Enter"
Clear done: tasks.filter(t => !t.done) in one line
Remaining count and summary text update on every render()

About this UI Snippet

Todo Widget — Task List with Progress Bar, Filter Tabs, Add Task & Clear Done

Screenshot of the Todo Widget snippet rendered live

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:

text
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

  1. 1
    Add 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.
  2. 2
    Complete 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%.
  3. 3
    Delete a taskHover over any task row to reveal the × delete button on the right. Click it to remove the task permanently from the list.
  4. 4
    Filter 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.
  5. 5
    Clear completed tasksClick "Clear done" in the footer to remove all completed tasks at once. Useful for keeping the list clean after a daily review.
  6. 6
    Persist 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

Personal and team dashboard task widgets
Embed a todo widget in any dashboard sidebar or card grid for lightweight task tracking. Users can manage daily tasks without leaving the main application — reducing context switching and improving focus during work sessions.
Sprint planning and daily standup task lists
Use the widget to display today's sprint tasks with done/remaining count, alongside a kanban board for stage tracking. The progress bar shows sprint completion at a glance. Filter to "Active" to focus on what is left, or "Done" to review what was completed during a standup.
Local-storage persisted personal todo apps
Add localStorage persistence to convert the widget into a standalone personal todo app. The JSON.stringify/JSON.parse pattern keeps task state across page reloads. Deploy as a browser home page or new tab extension for a lightweight daily task manager.
Onboarding checklist and setup guide widgets
Adapt the todo widget as an onboarding checklist: pre-populate tasks with setup steps (Connect your account, Add team members, Create your first project). Mark steps as done as the user completes them. Show the progress bar to motivate completion.
Learn re-render pattern and custom checkbox technique
The widget demonstrates the simplest state-driven rendering pattern in vanilla JavaScript: store state in an array, re-render the entire list on every change. The custom checkbox shows how to replace native inputs with CSS-only styled components using appearance: none and ::after.
Project management and task tracking dashboard panels
Wire the tasks array to a REST API: fetch tasks on load, POST new tasks, PATCH done state, DELETE removed tasks. The widget renders identically from API data as from static data — only the data source changes, not the rendering logic.

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.