Task list
Plan rows that tick through queued, running, done and failed.
Update the prompt and publish version 14
Applies both edits and starts a 20% rollout.
What happens when you approve
0 of 4- Accept partial invoice numbers with the supplier nameNot started
- Read the scheduled run date instead of “processing”Not started
- Re-run the payment status regression suiteNot started24 cases, about 3 minutes
- Publish version 14 to 20% of callsNot started
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function Hero() { const plan = [ { id: "p1", label: "Accept partial invoice numbers with the supplier name", }, { id: "p2", label: "Read the scheduled run date instead of “processing”", }, { id: "p3", label: "Re-run the payment status regression suite", detail: "24 cases, about 3 minutes", }, { id: "p4", label: "Publish version 14 to 20% of calls" }, ]; const [running, setRunning] = React.useState<number | null>(null); const finished = running !== null && running >= plan.length; React.useEffect(() => { if (running === null || running >= plan.length) return; const id = window.setTimeout( () => setRunning((n) => (n === null ? n : n + 1)), 750, ); return () => window.clearTimeout(id); }, [running, plan.length]); React.useEffect(() => { if (!finished) return; toast.add({ type: "success", title: "Version 14 is live", description: "Running on 20% of calls to the Payments desk agent.", }); }, [finished]); const tasks: TaskItem[] = plan.map((task, index) => ({ ...task, status: running === null || index > running ? "pending" : index < running ? "done" : "running", })); return ( <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border"> <div className="flex flex-col gap-0.5"> <h3 className="text-sm font-semibold text-foreground"> Update the prompt and publish version 14 </h3> <p className="text-13 text-muted-foreground"> Applies both edits and starts a 20% rollout. </p> </div> <TaskList title="What happens when you approve" tasks={tasks} className="rounded-[10px] bg-muted/70 p-3" /> <div className="flex justify-end gap-2"> {finished ? ( <Button type="button" variant="outline" size="sm" onClick={() => setRunning(null)} > Reset </Button> ) : ( <> <Button type="button" variant="ghost" size="sm" disabled={running !== null} onClick={() => toast.add({ title: "Skipped", description: "Version 13 stays live.", }) } > Skip </Button> <Button type="button" size="sm" disabled={running !== null} onClick={() => setRunning(0)} > Publish version 14 </Button> </> )} </div> </div> );}Usage#
Task list shows a plan as rows that tick from not started to in progress to done, with skipped and failed for plans that don't go to script. Each row has a status icon on a thin rail, a label and an optional detail, and a titled list counts 2 of 4 as it goes. It is how an agent shows what it is about to do and then does it, in front of the person who approved it. The common mistake is using it for reasoning or for tool calls: a task list is a plan with outcomes, so every row should be something the person would recognize as a step of the job.
When to use
- Under an approval, to say what will happen when someone approves: What happens when you approve.
- While an approved plan runs, advancing one row at a time until everything is done.
- While a generated artifact is built in a few visible stages, such as a report layout.
- For a campaign or batch with a few named phases that can fail on their own, such as an overdue W-9 collection.
When not to use
- For the model's reasoning before it answers. Use Thinking
- For individual tool calls with arguments and results. Use Tool chip
- For a single task with a known percentage done. Use Progress
- For the stage a record is in, such as a pipeline or onboarding stage. Use Stepper
- For a checklist a person ticks by hand. Use Checkbox
The Ink Fill Rule
The Label-Beside-Color Rule
The Tabular Figures Rule
Anatomy#
Overdue W-9 collection
1 of 3- Email the W-9 requestDone
- Call suppliers who haven't repliedIn progress7 of 12 called
- Attach signed formsNot started
- Title. Optional 13px medium heading, rendered as an
h3. - Count. 12px Slate Meta done of total at the right, when both
titleandtasksare set. Skipped rows count as done. - Status icon. 16px: a dashed ring for not started, a spinning arc for in progress, a filled ink check for done, a dashed-through ring for skipped, a red exclamation for failed.
- Rail. A 1px hairline from each icon down to the next row, hidden after the last row.
- Label. 13px. Medium weight with the shimmer while in progress, Slate Meta once done, struck through when skipped.
- Detail. Optional 12px Slate Meta line under the label. Signal Red on a failed row.
Examples#
Statuses
Not started, in progress, done, skipped and failed. Each has its own icon shape; only failed turns red.
- Email the W-9 request to 48 suppliersDone
- Call the 12 who haven't opened itIn progress7 of 12 called
- Attach signed forms to supplier recordsNot started
- Text suppliers without an emailSkippedEvery supplier has an email on file.
- Hold Friday's payments for missing W-9sFailedThe ERP rejected the hold. Nothing was changed.
import { TaskList } from "@oration/canon/components/ai/task-list";export function Statuses() { return ( <div className="w-full max-w-sm"> <TaskList tasks={[ { id: "s1", label: "Email the W-9 request to 48 suppliers", status: "done", }, { id: "s2", label: "Call the 12 who haven't opened it", status: "running", detail: "7 of 12 called", }, { id: "s3", label: "Attach signed forms to supplier records", status: "pending", }, { id: "s4", label: "Text suppliers without an email", status: "skipped", detail: "Every supplier has an email on file.", }, { id: "s5", label: "Hold Friday's payments for missing W-9s", status: "error", detail: "The ERP rejected the hold. Nothing was changed.", }, ]} /> </div> );}Untitled, while something is built
Without a title there is no count, just the rows. Here a report draft is laid out in three visible stages, then a toast confirms it.
Describe the report you want
A Monday review of transfers by agent and why suppliers get handed off.
- Reading your requestIn progress
- Choosing metrics and breakdownsNot started
- Laying out insightsNot started
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function InACard() { const steps = [ "Reading your request", "Choosing metrics and breakdowns", "Laying out insights", ]; const [step, setStep] = React.useState(0); React.useEffect(() => { if (step > steps.length) return; const id = window.setTimeout(() => { if (step === steps.length) { toast.add({ type: "success", title: "Draft layout added", description: "Drag to reorder, or open any insight to change it.", }); } setStep((n) => n + 1); }, 700); return () => window.clearTimeout(id); }, [step, steps.length]); const tasks: TaskItem[] = steps.map((label, index) => ({ id: label, label, status: step > index ? "done" : step === index ? "running" : "pending", })); return ( <section className="w-full max-w-md rounded-xl bg-card p-4 text-left shadow-border"> <h3 className="text-sm font-semibold text-foreground"> Describe the report you want </h3> <p className="mt-0.5 text-13 text-muted-foreground"> A Monday review of transfers by agent and why suppliers get handed off. </p> <div className="mt-3"> <TaskList tasks={tasks} /> </div> </section> );}Composed rows
Pass TaskRow children when labels need markup. Composed lists don't get the count or the status announcement, so the title stands alone.
Overdue W-9 collection
- Email the request to 48 suppliersDoneSent from payments@cedarline.example on Monday, Sep 28.
- Call the 12 who haven't opened itIn progress7 of 12 called
- Attach forms as w9-signed.pdfNot started
import { TaskList, TaskRow } from "@oration/canon/components/ai/task-list";export function Composed() { return ( <div className="w-full max-w-sm"> <TaskList title="Overdue W-9 collection"> <TaskRow status="done" label={ <> Email the request to{" "} <span className="font-medium">48 suppliers</span> </> } detail="Sent from payments@cedarline.example on Monday, Sep 28." /> <TaskRow status="running" label="Call the 12 who haven't opened it" detail={ <span className="tabular-nums">7 of 12 called</span> } /> <TaskRow status="pending" label={ <> Attach forms as{" "} <span className="font-mono text-xs"> w9-signed.pdf </span> </> } /> </TaskList> </div> );}A step that fails
The failed row says why in its detail and the step that depended on it is skipped. Retrying moves both forward.
Overdue W-9 collection
2 of 3- Email the W-9 request to 48 suppliersDone
- Call the 12 who haven't opened itFailed3 suppliers have no phone number on file. The other 9 were called.
- Attach signed forms to supplier recordsSkipped
import { type TaskItem, TaskList } from "@oration/canon/components/ai/task-list";import { Button } from "@oration/canon/components/button";import * as React from "react";export function Failure() { const [retried, setRetried] = React.useState<"failed" | "running" | "done">( "failed", ); React.useEffect(() => { if (retried !== "running") return; const id = window.setTimeout(() => setRetried("done"), 1400); return () => window.clearTimeout(id); }, [retried]); const tasks: TaskItem[] = [ { id: "f1", label: "Email the W-9 request to 48 suppliers", status: "done", }, { id: "f2", label: "Call the 12 who haven't opened it", status: retried === "failed" ? "error" : retried, detail: retried === "failed" ? "3 suppliers have no phone number on file. The other 9 were called." : retried === "running" ? "Calling the 3 with numbers added" : "All 12 reached", }, { id: "f3", label: "Attach signed forms to supplier records", status: retried === "done" ? "done" : retried === "failed" ? "skipped" : "pending", }, ]; return ( <div className="flex w-full max-w-sm flex-col gap-3"> <TaskList title="Overdue W-9 collection" tasks={tasks} /> {retried === "failed" ? ( <Button type="button" variant="outline" size="sm" className="self-start" onClick={() => setRetried("running")} > Retry calls </Button> ) : null} </div> );}States#
| State | Treatment |
|---|---|
| Not started | A dashed Firm Hairline ring. status="pending". |
| In progress | A spinning arc and a shimmering medium-weight label. status="running". |
| Done | A filled Ink 80 check and a Slate Meta label. The circle scales in and the check draws itself, but only when the row turns done on screen; rows that load done render still. |
| Skipped | A ring with a dash and a struck-through Slate Meta label. Counts toward done. |
| Failed | A 12% red disc with a red exclamation; the detail turns red. Doesn't count toward done. status="error". |
| Reduced motion | The spinner and shimmer stop. The check still appears, without the scale. |
Behavior#
TaskListis presentational. Status comes from you: advance rows on a timer for a simulation, or from real job events.- Pass
tasksto render rows plus the count and a status announcement, or composeTaskRowchildren for labels with markup. Children get neither the count nor the announcement. - A row remembers its status at mount. If it mounts done, the check renders still; if it turns done later, the circle scales in on
spring.moderateand the check path draws onspring.slow. - The rail is drawn per row, so rows can be added or removed without redrawing a single long line.
- The spinner uses
animate-spinwithmotion-reduce:animate-none.
Do and don't#
What happens when you approve
1 of 3- Accept partial invoice numbersDone
- Re-run the regression suiteIn progress
- Publish version 14 to 20% of callsNot started
What happens when you approve
4 of 8- Open the ERPDone
- Click searchDone
- Type the invoice numberDone
- Read the resultDone
- Open the promptIn progress
- Find the paragraphNot started
- Edit the paragraphNot started
- SaveNot started
- Call the 12 who haven't opened itFailed3 suppliers have no phone number on file.
- Attach signed formsSkipped
- Call the 12 who haven't opened itFailed
- Attach signed formsIn progress
Content#
- Write each row as the action, verb first, in sentence case: Email the W-9 request to 48 suppliers, Publish version 14 to 20% of calls.
- Keep the label the same as the row changes status. The icon says whether it is done; don't rewrite the label into the past tense.
- Use
detailfor size or outcome: 24 cases, about 3 minutes, 7 of 12 called. - On a failure, the detail says what went wrong and what happens next: 3 suppliers have no phone number on file. They'll get a second email instead.
- Title the list for what it is about to do: What happens when you approve, Overdue W-9 collection.
Accessibility#
- The list is an
<ol>, so screen readers announce the number of steps and each position. - Each row carries a visually hidden status word: Not started, In progress, Done, Skipped or Failed.
- With
tasks, a hiddenrole="status"announces 2 of 4 steps done as the count changes. Composed children don't get this; add your own status region if progress matters. - Icons are
aria-hidden. The spinner and shimmer stop under reduced motion. - The title renders as an
h3. Check it fits the heading outline where you place the list.
Design tokens#
| Token | Used for |
|---|---|
--foreground | Done disc at 80%, spinner arc at 80%, labels |
--background | The check drawn on the done disc |
--border-strong | Not started and skipped rings, the spinner track |
--border | The rail between rows |
--muted-foreground | Done and skipped labels, details, the count |
--subtle-foreground | The skipped dash and strikethrough |
--destructive | Failed disc at 12%, its mark and detail |
text-shimmer | The in-progress label |
spring.moderate, spring.slow | The done circle and the check draw |
API reference#
TaskList
The list, with an optional title and count.
Other props spread onto Nothing. Only the props below are read..
| Prop | Type | Default | Description |
|---|---|---|---|
tasks | TaskItem[] | No default | { id, label, status, detail? }[]. Renders rows, the count and the status announcement. |
title | string | No default | Heading above the rows. With tasks, the count sits beside it. |
children | React.ReactNode | No default | TaskRow elements, rendered after any tasks rows. |
className | string | No default | Merged onto the root. |
TaskRow
One row, for composing a list by hand.
Other props spread onto Nothing.
| Prop | Type | Default | Description |
|---|---|---|---|
labelRequired | React.ReactNode | No default | The step. Can hold markup. |
statusRequired | "pending" | "running" | "done" | "skipped" | "error" | No default | Not started, in progress, done, skipped or failed. |
detail | React.ReactNode | No default | Line under the label. |
className | string | No default | Merged onto the <li>. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The product only uses not started, in progress and done. Skipped and failed exist but nothing renders them, so an approved Copilot plan has no way to show a failed step.
Composed TaskRow children get no count and no status announcement.
The title is always an h3, which can skip a level inside a panel whose section heading is an h3 too.
A failed row keeps its label in ink; only the icon and detail turn red.