Onboarding
Checklist card
Getting-started tasks with progress
Getting-started tasks with progress
Create a getting-started checklist card with Tailwind CSS: a title with a progress label (e.g. "2 of 4 done"), a thin progress bar, and a list of tasks. Completed tasks show a filled check and strikethrough muted text; incomplete tasks show an empty circle and an action link on the right. Then add JavaScript that lets the list be completed: clicking a task or its action link toggles it done, which fills the marker, strikes the label, hides the action, animates the progress bar to the new percentage, and updates the "n of m done" count — with a small celebratory line replacing the count once everything is checked. <div class="mx-auto max-w-md p-6">
<div class="rounded-2xl border border-neutral-200 bg-white p-6">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-neutral-900">Get started</h2>
<span id="progressLabel" class="text-xs text-neutral-500">2 of 4 done</span>
</div>
<div class="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-neutral-100">
<div id="progressBar" class="h-full w-1/2 rounded-full bg-neutral-900 transition-all duration-300"></div>
</div>
<ul id="tasks" class="mt-5 space-y-3 text-sm">
<li data-done="true" class="flex cursor-pointer items-center justify-between gap-3">
<span class="flex items-center gap-3"><span data-marker class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-900 text-[10px] text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m5 12.5 4.25 4.25L19 7"/></svg></span><span data-label class="text-neutral-400 line-through">Create your account</span></span>
<a href="#" data-action class="hidden font-medium text-neutral-900 hover:underline">Start</a>
</li>
<li data-done="true" class="flex cursor-pointer items-center justify-between gap-3">
<span class="flex items-center gap-3"><span data-marker class="flex h-5 w-5 items-center justify-center rounded-full bg-neutral-900 text-[10px] text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m5 12.5 4.25 4.25L19 7"/></svg></span><span data-label class="text-neutral-400 line-through">Verify your email</span></span>
<a href="#" data-action class="hidden font-medium text-neutral-900 hover:underline">Resend</a>
</li>
<li data-done="false" class="flex cursor-pointer items-center justify-between gap-3">
<span class="flex items-center gap-3"><span data-marker class="h-5 w-5 rounded-full border-2 border-neutral-300"></span><span data-label class="text-neutral-700">Invite your team</span></span>
<a href="#" data-action class="font-medium text-neutral-900 hover:underline">Invite</a>
</li>
<li data-done="false" class="flex cursor-pointer items-center justify-between gap-3">
<span class="flex items-center gap-3"><span data-marker class="h-5 w-5 rounded-full border-2 border-neutral-300"></span><span data-label class="text-neutral-700">Connect an integration</span></span>
<a href="#" data-action class="font-medium text-neutral-900 hover:underline">Connect</a>
</li>
</ul>
</div>
</div> var tasks = [].slice.call(document.querySelectorAll("#tasks li"));
var bar = document.getElementById("progressBar");
var label = document.getElementById("progressLabel");
var CHECKED = "flex h-5 w-5 items-center justify-center rounded-full bg-neutral-900 text-[10px] text-white";
var UNCHECKED = "h-5 w-5 rounded-full border-2 border-neutral-300";
function paint(task) {
var done = task.getAttribute("data-done") === "true";
var marker = task.querySelector("[data-marker]");
var text = task.querySelector("[data-label]");
var action = task.querySelector("[data-action]");
marker.className = done ? CHECKED : UNCHECKED;
marker.textContent = done ? "✓" : "";
text.className = done ? "text-neutral-400 line-through" : "text-neutral-700";
action.classList.toggle("hidden", done);
}
function render() {
var done = tasks.filter(function (t) { return t.getAttribute("data-done") === "true"; }).length;
bar.style.width = (done / tasks.length) * 100 + "%";
label.textContent =
done === tasks.length ? "All done — nice work" : done + " of " + tasks.length + " done";
}
tasks.forEach(function (task) {
paint(task);
task.addEventListener("click", function (e) {
e.preventDefault(); // the action link is a demo affordance, not a destination
task.setAttribute("data-done", task.getAttribute("data-done") === "true" ? "false" : "true");
paint(task);
render();
});
});
render();