Dashboard Tasks
Original · freetasks/checklist widget
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/dash-tasks.json"use client";
import {
useId,
useMemo,
useRef,
useState,
type FormEvent,
type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Priority = "high" | "medium" | "low";
type Filter = "all" | "active" | "done";
type Task = {
id: string;
title: string;
tag: string;
due: string;
priority: Priority;
assignee: string;
initials: string;
done: boolean;
};
const INITIAL_TASKS: Task[] = [
{
id: "t-1",
title: "Ship the pricing page A/B test to 50% of traffic",
tag: "Growth",
due: "Today",
priority: "high",
assignee: "Salman Khan",
initials: "SK",
done: false,
},
{
id: "t-2",
title: "Trace the Lighthouse regression on /blog after the font swap",
tag: "Perf",
due: "Tomorrow",
priority: "medium",
assignee: "Ana Reyes",
initials: "AR",
done: false,
},
{
id: "t-3",
title: "Write migration notes for the session-auth rewrite",
tag: "Docs",
due: "Fri",
priority: "low",
assignee: "Dev Mehta",
initials: "DM",
done: false,
},
{
id: "t-4",
title: "Reply to the 14 Figma plugin beta testers",
tag: "Support",
due: "Wed",
priority: "medium",
assignee: "Ana Reyes",
initials: "AR",
done: true,
},
{
id: "t-5",
title: "Cut the 2.4.0 release branch and freeze main",
tag: "Release",
due: "Mon",
priority: "high",
assignee: "Salman Khan",
initials: "SK",
done: false,
},
{
id: "t-6",
title: "Delete the legacy S3 bucket now that the CDN is live",
tag: "Infra",
due: "Done",
priority: "low",
assignee: "Dev Mehta",
initials: "DM",
done: true,
},
];
const PRIORITY_BADGE: Record<Priority, string> = {
high: "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300",
medium:
"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300",
low: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/10 dark:text-sky-300",
};
const PRIORITY_BAR: Record<Priority, string> = {
high: "bg-rose-500",
medium: "bg-amber-500",
low: "bg-sky-500",
};
const AVATAR_TONE: Record<string, string> = {
SK: "bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
AR: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
DM: "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
NEW: "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
};
const FILTERS: { id: Filter; label: string }[] = [
{ id: "all", label: "All" },
{ id: "active", label: "Active" },
{ id: "done", label: "Done" },
];
const RING_RADIUS = 26;
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
export default function DashTasks() {
const [tasks, setTasks] = useState<Task[]>(INITIAL_TASKS);
const [filter, setFilter] = useState<Filter>("all");
const [draft, setDraft] = useState<string>("");
const [draftPriority, setDraftPriority] = useState<Priority>("medium");
const [error, setError] = useState<string>("");
const [announcement, setAnnouncement] = useState<string>("");
const baseId = useId();
const counter = useRef<number>(100);
const inputRef = useRef<HTMLInputElement | null>(null);
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
const reduce = useReducedMotion();
const doneCount = tasks.filter((t) => t.done).length;
const total = tasks.length;
const activeCount = total - doneCount;
const pct = total === 0 ? 0 : Math.round((doneCount / total) * 100);
const counts: Record<Filter, number> = {
all: total,
active: activeCount,
done: doneCount,
};
const visible = useMemo<Task[]>(() => {
if (filter === "active") return tasks.filter((t) => !t.done);
if (filter === "done") return tasks.filter((t) => t.done);
return tasks;
}, [tasks, filter]);
const toggle = (id: string) => {
setTasks((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
);
const target = tasks.find((t) => t.id === id);
if (target) {
setAnnouncement(
`${target.title} marked ${target.done ? "active" : "complete"}`,
);
}
};
const remove = (id: string) => {
const target = tasks.find((t) => t.id === id);
setTasks((prev) => prev.filter((t) => t.id !== id));
if (target) setAnnouncement(`${target.title} deleted`);
};
const clearCompleted = () => {
if (doneCount === 0) return;
setTasks((prev) => prev.filter((t) => !t.done));
setAnnouncement(
`Cleared ${doneCount} completed ${doneCount === 1 ? "task" : "tasks"}`,
);
};
const addTask = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const title = draft.trim();
if (title.length === 0) {
setError("Give the task a name before adding it.");
inputRef.current?.focus();
return;
}
counter.current += 1;
const task: Task = {
id: `t-${counter.current}`,
title,
tag: "Inbox",
due: "Today",
priority: draftPriority,
assignee: "Unassigned",
initials: "NEW",
done: false,
};
setTasks((prev) => [task, ...prev]);
setDraft("");
setError("");
setAnnouncement(`${title} added to the list`);
inputRef.current?.focus();
};
const onTabKeyDown = (e: KeyboardEvent<HTMLButtonElement>, i: number) => {
const last = FILTERS.length - 1;
let target = -1;
if (e.key === "ArrowRight") target = i === last ? 0 : i + 1;
else if (e.key === "ArrowLeft") target = i === 0 ? last : i - 1;
else if (e.key === "Home") target = 0;
else if (e.key === "End") target = last;
if (target >= 0) {
e.preventDefault();
setFilter(FILTERS[target].id);
tabRefs.current[target]?.focus();
}
};
const panelId = `${baseId}-panel`;
const emptyCopy: Record<Filter, string> = {
all: "Nothing on the board. Add the first task above.",
active: "No open tasks left — the sprint board is clear.",
done: "Nothing completed yet. Tick something off to see it here.",
};
return (
<section className="relative w-full bg-slate-50 px-4 py-20 sm:px-6 sm:py-24 dark:bg-slate-950">
<style>{`
@keyframes dshtsk-pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.12); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes dshtsk-sheen {
from { opacity: 0.35; }
to { opacity: 0.7; }
}
.dshtsk-check { animation: dshtsk-pop 240ms cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.dshtsk-ring { transition: stroke-dashoffset 520ms cubic-bezier(0.22, 1, 0.36, 1); }
.dshtsk-glow { animation: dshtsk-sheen 4s ease-in-out infinite alternate; }
.dshtsk-title {
background-image: linear-gradient(currentColor, currentColor);
background-repeat: no-repeat;
background-position: 0 58%;
background-size: 0% 1.5px;
transition: background-size 300ms ease, color 200ms ease;
}
.dshtsk-title[data-done="true"] { background-size: 100% 1.5px; }
@media (prefers-reduced-motion: reduce) {
.dshtsk-check { animation: none !important; }
.dshtsk-ring { transition: none !important; }
.dshtsk-glow { animation: none !important; }
.dshtsk-title { transition: none !important; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
{/* Header */}
<div className="relative flex items-start justify-between gap-6 border-b border-slate-200 px-5 py-5 sm:px-6 dark:border-slate-800">
<div
aria-hidden="true"
className="dshtsk-glow pointer-events-none absolute -top-24 -left-16 h-48 w-48 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/15"
/>
<div className="relative">
<span className="inline-flex items-center gap-1.5 rounded-full bg-indigo-100 px-2.5 py-1 text-[11px] font-semibold tracking-wide text-indigo-700 uppercase dark:bg-indigo-500/15 dark:text-indigo-300">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Sprint 24
</span>
<h2 className="mt-2.5 text-lg font-semibold tracking-tight text-slate-900 sm:text-xl dark:text-white">
Team checklist
</h2>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
{activeCount} open · {doneCount} done · ends Friday 6pm
</p>
</div>
<div className="relative shrink-0">
<svg
width="68"
height="68"
viewBox="0 0 68 68"
aria-hidden="true"
className="-rotate-90"
>
<circle
cx="34"
cy="34"
r={RING_RADIUS}
fill="none"
strokeWidth="6"
className="stroke-slate-200 dark:stroke-slate-800"
/>
<circle
cx="34"
cy="34"
r={RING_RADIUS}
fill="none"
strokeWidth="6"
strokeLinecap="round"
strokeDasharray={RING_CIRCUMFERENCE}
strokeDashoffset={
RING_CIRCUMFERENCE - (RING_CIRCUMFERENCE * pct) / 100
}
className="dshtsk-ring stroke-indigo-500 dark:stroke-indigo-400"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-sm font-semibold tabular-nums text-slate-900 dark:text-white">
{pct}%
</span>
<span className="sr-only">{pct} percent of tasks complete</span>
</div>
</div>
{/* Add task */}
<form onSubmit={addTask} className="border-b border-slate-200 px-5 py-4 sm:px-6 dark:border-slate-800">
<div className="flex flex-col gap-2 sm:flex-row">
<div className="relative flex-1">
<label htmlFor={`${baseId}-new`} className="sr-only">
New task title
</label>
<input
id={`${baseId}-new`}
ref={inputRef}
type="text"
value={draft}
onChange={(e) => {
setDraft(e.target.value);
if (error) setError("");
}}
placeholder="Add a task — e.g. audit the onboarding emails"
aria-invalid={error.length > 0}
aria-describedby={error ? `${baseId}-err` : undefined}
className={`w-full rounded-lg border bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:bg-slate-950 dark:text-white dark:placeholder:text-slate-500 ${
error
? "border-rose-400 dark:border-rose-500/60"
: "border-slate-300 dark:border-slate-700"
}`}
/>
</div>
<div className="relative">
<label htmlFor={`${baseId}-prio`} className="sr-only">
Priority for the new task
</label>
<select
id={`${baseId}-prio`}
value={draftPriority}
onChange={(e) => setDraftPriority(e.target.value as Priority)}
className="w-full appearance-none rounded-lg border border-slate-300 bg-white py-2 pr-9 pl-3 text-sm font-medium text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:w-auto dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200"
>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="pointer-events-none absolute top-1/2 right-3 h-3.5 w-3.5 -translate-y-1/2 text-slate-400"
>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
<button
type="submit"
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-indigo-600 px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 active:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
className="h-4 w-4"
>
<path d="M12 5v14M5 12h14" />
</svg>
Add
</button>
</div>
{error ? (
<p
id={`${baseId}-err`}
className="mt-2 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{error}
</p>
) : null}
</form>
{/* Filters */}
<div className="flex items-center justify-between gap-3 border-b border-slate-200 px-5 py-3 sm:px-6 dark:border-slate-800">
<div
role="tablist"
aria-label="Filter tasks"
className="flex gap-1 rounded-lg bg-slate-100 p-1 dark:bg-slate-950"
>
{FILTERS.map((f, i) => {
const selected = filter === f.id;
return (
<button
key={f.id}
ref={(el) => {
tabRefs.current[i] = el;
}}
type="button"
role="tab"
id={`${baseId}-tab-${f.id}`}
aria-selected={selected}
aria-controls={panelId}
tabIndex={selected ? 0 : -1}
onClick={() => setFilter(f.id)}
onKeyDown={(e) => onTabKeyDown(e, i)}
className={`relative rounded-md px-3 py-1.5 text-xs font-semibold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
selected
? "bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{f.label}
<span className="ml-1.5 tabular-nums opacity-60">
{counts[f.id]}
</span>
</button>
);
})}
</div>
<button
type="button"
onClick={clearCompleted}
disabled={doneCount === 0}
className="rounded-md px-2 py-1.5 text-xs font-semibold text-slate-500 transition-colors hover:text-rose-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-slate-500 dark:text-slate-400 dark:hover:text-rose-400 dark:disabled:hover:text-slate-400"
>
Clear done
</button>
</div>
{/* List */}
<div
id={panelId}
role="tabpanel"
aria-labelledby={`${baseId}-tab-${filter}`}
className="px-3 py-3 sm:px-4"
>
{visible.length === 0 ? (
<p className="px-3 py-10 text-center text-sm text-slate-500 dark:text-slate-400">
{emptyCopy[filter]}
</p>
) : (
<ul className="flex flex-col gap-1.5">
<AnimatePresence initial={false}>
{visible.map((t) => {
const titleId = `${baseId}-title-${t.id}`;
return (
<motion.li
key={t.id}
layout={!reduce}
initial={reduce ? false : { opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, x: 14 }}
transition={{
duration: reduce ? 0 : 0.22,
ease: "easeOut",
}}
className="group relative flex items-start gap-3 overflow-hidden rounded-xl border border-slate-200 bg-white p-3 transition-colors hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700"
>
<span
aria-hidden="true"
className={`absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full transition-opacity ${
PRIORITY_BAR[t.priority]
} ${t.done ? "opacity-25" : "opacity-100"}`}
/>
<label className="flex flex-1 cursor-pointer items-start gap-3 pl-2">
<input
type="checkbox"
checked={t.done}
onChange={() => toggle(t.id)}
aria-labelledby={titleId}
className="peer sr-only"
/>
<span
aria-hidden="true"
className={`mt-0.5 flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-md border-2 transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-indigo-500 ${
t.done
? "border-emerald-500 bg-emerald-500 text-white"
: "border-slate-300 bg-white text-transparent group-hover:border-indigo-400 dark:border-slate-600 dark:bg-slate-950"
}`}
>
{t.done ? (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
className="dshtsk-check h-3 w-3"
>
<path d="M20 6 9 17l-5-5" />
</svg>
) : null}
</span>
<span className="min-w-0 flex-1">
<span
id={titleId}
data-done={t.done}
className={`dshtsk-title inline text-sm leading-snug font-medium ${
t.done
? "text-slate-400 dark:text-slate-500"
: "text-slate-900 dark:text-slate-100"
}`}
>
{t.title}
</span>
<span className="mt-2 flex flex-wrap items-center gap-1.5">
<span
className={`inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-semibold tracking-wide uppercase ${PRIORITY_BADGE[t.priority]}`}
>
{t.priority}
</span>
<span className="inline-flex items-center rounded-md border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[10px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{t.tag}
</span>
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-slate-500 dark:text-slate-400">
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className="h-3 w-3"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 7v5l3 2" />
</svg>
{t.due}
</span>
<span
className={`ml-auto inline-flex h-5 items-center rounded-full px-1.5 text-[10px] font-bold tracking-wide ${
AVATAR_TONE[t.initials] ?? AVATAR_TONE.NEW
}`}
>
<span aria-hidden="true">{t.initials}</span>
<span className="sr-only">
Assigned to {t.assignee}
</span>
</span>
</span>
</span>
</label>
<button
type="button"
onClick={() => remove(t.id)}
aria-label={`Delete task: ${t.title}`}
className="mt-0.5 shrink-0 rounded-md p-1 text-slate-400 opacity-0 transition-all hover:bg-rose-50 hover:text-rose-600 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 group-hover:opacity-100 dark:hover:bg-rose-500/10 dark:hover:text-rose-400"
>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
>
<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6" />
<path d="M10 11v5M14 11v5" />
</svg>
</button>
</motion.li>
);
})}
</AnimatePresence>
</ul>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between gap-3 border-t border-slate-200 px-5 py-3 text-xs text-slate-500 sm:px-6 dark:border-slate-800 dark:text-slate-400">
<span>
Showing{" "}
<span className="font-semibold text-slate-700 tabular-nums dark:text-slate-200">
{visible.length}
</span>{" "}
of {total}
</span>
<span className="inline-flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Synced 2 min ago
</span>
</div>
</div>
<p aria-live="polite" className="sr-only">
{announcement}
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Dashboard KPI Row
Originalrow of KPI stat cards with trends

Dashboard Stat Widgets
Originalassorted dashboard stat widgets

Dashboard Activity
Originalrecent activity feed widget

Dashboard Chart Widget
Originalchart panel widget with tabs

Dashboard Revenue
Originalrevenue overview card with chart

Dashboard Users
Originalactive users widget

Dashboard Calendar Widget
Originalmini calendar dashboard widget

Dashboard Notifications
Originalnotifications panel widget

Dashboard Overview
Originalcompact dashboard overview grid

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

