PlanList
An agent's plan as a live checklist — ordered steps that arrive and change state mid-stream, with nested sub-steps, a per-step result line and batched screen-reader announcements.
pnpm add @tessinaui/uiThe card above is a plan mid-run: two steps done, the third failed with its reason and Retry and Skip under it. Press either and the run carries on to the summary. For the whole agent flow — drafting, reordering and approving a plan before it runs — see the Agent Plan template, which is built from this component.
When to use
- An agent's plan beside its output. The steps update in place as the agent works, so people can see where it is without reading the transcript.
- A run of tasks that each finish on their own. Every step can be
done,failedorskippedindependently of the others. - An ordered checklist whose state arrives over time rather than all at once, sent as a whole snapshot on every render.
- Steps with a result worth showing. A short result line (a count, a file, a duration) sits under the label, and sub-steps nest one level of detail beneath it.
- A plan that is read by assistive technology too. State is text in every row and changes are announced in batches, not one by one.
When not to use
- Inside a chat bubble. Use
ChatBubbleSteps/ChatBubbleStep(see ChatBubble): a collapsible disclosure of what the model already did, with no positions, no batching and no live region of its own. - For a form wizard. A plan is not navigable; a wizard's steps are.
- For one long operation. Use Progress; a plan is a list of operations.
- For a quantity control. Stepper is the numeric +/- control, not a step list.
Installation
pnpm add @tessinaui/uiUsage
import { PlanList } from "@tessinaui/ui/plan-list";
<PlanList
aria-label="Plan"
steps={[
{ id: "read", label: "Read the repository", status: "done", result: "42 files" },
{ id: "search", label: "Search for callers", status: "running" },
{ id: "patch", label: "Apply the patch" },
]}
/>The plan is a snapshot, never a script of events: pass the whole array on every render, as a streaming agent sends it, and the component works out what changed.
Examples
Playground
Default
States
All five states, and none of them is carried by colour alone. showStatus
adds the state as a word at the end of each row, so every glyph has its word.
Sub-steps
Failure
A failed step shows its reason, and its actions offer Retry and Skip in
place. When the button you pressed goes away, focus moves to the step's label,
which now says its new state.
Variants
The default rows are a checklist. Three more anatomies draw the same steps for
a different surface; pick by where the plan sits.
detailedis a plan card's rows. It has a wide glyph column, the step'sactivitylines while it runs or after it fails (they fold away once it is done), a failure block with its controls, thedurationat the end, and anoteline naming who added a step.compactis one centred line per step, inked by state: the running step in full ink, a failed one in the error ink, the rest muted.activityleads each row with its tool glyph and carries the state in the label's ink, with thedurationtrailing. Setcollapsedon an older step to fold it away.
The variants draw state with the shared agent status glyph (the same set a
ToolCall uses), so a running step is a small
spinner there, where the default row keeps its calmer half disc. A step added
after the plan was written takes inserted and arrives on a wash that fades.
Progress and summary
showProgress counts finished steps — done and skipped, top level only —
over a thin bar. summary closes the plan once it is done.
Streaming
A step that arrives after the list first paints opens its own height, and a changed status word fades in.
In a narrow card
On color
Right to left
dir="rtl" mirrors the rows and sub-step indent and swaps in the Arabic
strings, both the sr-only step prefix and the announcements. dir does not
set a language: pass lang="ar" with it, or a screen reader reads the Arabic
in the page's voice. For another right-to-left language, override every entry
in strings.
Loading
API Reference
PlanList
| Prop | Type | Default | Description |
|---|---|---|---|
steps | PlanStep[] | — | The plan. Any prefix of it renders |
variant | "default" | "detailed" | "compact" | "activity" | "default" | Row anatomy. detailed: a plan card's rows — a wide glyph column, activity lines while a step works, a failure block with its controls, the time at the end. compact: one centred line per step, inked by state. activity: each row leads with its tool glyph, state carried by the label's ink, the time trailing |
tone | "default" | "on-color" | inherited | Inherited from a wrapping <Surface>, else "default"; an explicit prop wins |
dir | string | — | Text direction; "rtl" also picks the Arabic strings |
strings | Partial<PlanListStrings> | — | Per-entry string overrides |
showStatus | boolean | false | Shows each step's state as a word at the row's end. Not spoken — the row's hidden prefix already says it |
showProgress | boolean | false | Shows "2 of 6 steps finished" over a thin bar above the steps. Counts top-level done and skipped |
summary | ReactNode | — | A closing line under the steps. Announced once when it appears after the first paint |
summaryText | string | the summary when it is a string | Plain text of summary for the announcement |
className | string | — | Additional classes on the root wrapper |
aria-* and role land on the <ol>; everything else lands on the root.
PlanStep
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable identity across renders — the key the announcer diffs on |
label | ReactNode | — | The visible step label |
labelText | string | the label when it is a string | Plain text for announcements; required when label is a node |
status | PlanStepStatus | "pending" | pending · running · done · failed · skipped |
result | ReactNode | — | Result line under the label — a count, a file, a duration |
error | ReactNode | — | Why a step failed; a string is also read out in the announcement |
icon | ReactNode | — | Overrides the status glyph — e.g. <Spinner label={null} /> |
substeps | PlanStep[] | — | Nested sub-steps, rendered as a nested ordered list |
activity | { id?, icon?, text }[] | — | What the step did — short lines with a tool glyph. detailed shows them while the step runs or has failed, and folds them away after |
duration | ReactNode | — | Trailing time, at the row's end in detailed and activity rows |
note | ReactNode | — | One line under the label naming who added the step. detailed rows |
inserted | boolean | false | The step was added after the plan was written: it arrives on a wash that fades |
collapsed | boolean | false | Folds the row away and hides it — an older step scrolled out of a short list. activity rows |
actions | ReactNode | — | Controls under the step — Retry and Skip on a failed one. If the focused one leaves, focus moves to the step's label |
PlanListSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
items | number | 3 | Placeholder rows |
dir | string | — | Text direction |
label | string | "Loading plan", Arabic under dir="rtl" | What AT announces while the placeholder is up — rendered as sr-only text, not aria-label |
showProgress | boolean | false | Reserves the progress line, matching PlanList showProgress |
showStatus | boolean | false | Reserves each row's status word, matching PlanList showStatus |
DOM contract
| Attribute | Where | Values |
|---|---|---|
data-slot | every part | plan-list, plan-list-progress, plan-list-steps, plan-list-step, plan-list-label, plan-list-status, plan-list-substeps, plan-list-result, plan-list-error, plan-list-actions, plan-list-activity, plan-list-duration, plan-list-note, plan-list-failure, plan-list-summary, plan-list-announcer, plan-list-skeleton |
data-status | every <li> | pending · running · done · failed · skipped |
aria-current | the running <li> | step |
What it's made of
| Part | What it is |
|---|---|
| Steps | An ordered list; sub-steps a nested ordered list |
| Status glyph | Five shapes: a ring, a half disc, a ring with a check, a ring with a cross, a ring with a dash. The detailed, compact and activity rows use the shared agent status set, where running is a spinner |
| Status word | Text at the row's end with showStatus |
| Result and failure lines | Text under the label; a failure in the failure ink |
| Actions | Your controls — usually two Buttons, xs |
| Progress line | A line of text over a Progress bar, xs |
| Summary | Text under the steps |
States
A step is pending, running, done, failed or skipped. Each has its
own shape, and with showStatus its own word too, so none rests on colour.
A failed step shows its reason and, when you pass them, its actions. A
skipped step is struck through. The plan itself has no state of its own: it is
running while a step runs, done when every step is done or skipped, and
you say so with summary.
Motion
Every movement explains a change, and nothing moves when the list first paints.
- A step, a failure line, its actions or the summary that arrives later opens its own height on the panel curve.
- A changed status word, glyph or progress count fades in on the short popup clock.
- A running step's glyph breathes gently.
With reduced motion every state, word, focus move and announcement stays; the movement and the fades go.
Accessibility
There is no APG pattern for a list of steps with status — the index has 32
patterns and none of them is this — so the contract here is list semantics plus
aria-current="step",
which is what the systems that ship something similar converge on.
- Real list semantics: an
<ol>of<li>s, sub-steps in a nested<ol>. - State is text, inside the row. Every row opens with an
sr-onlyprefix — "Step 2 of 5, in progress: " — so a reader walking the list hears the position, the state and the label together. It is deliberately not anaria-labelon the<li>: alistitemtakes its name from the author only, so a label would replace the row's content in the accessibility tree and take the result and failure lines with it. (Other step indicators do the same thing with their own visually-hidden span.) - State is never colour alone. The five states are five different shapes,
so they survive greyscale, colour-blindness and a high-contrast theme, and
data-statuscarries the same fact to the DOM. Glyphs arearia-hidden. - Announcements are batched. Transitions are collected for 1.5s and spoken
as ONE sentence through one always-mounted polite region — "Search for
callers: done. Apply the patch: in progress." Above three steps changing to
the same state it collapses to a count ("4 steps done"). Nothing here uses
role="alert". - The first paint is history. Whatever the plan looks like when it mounts
— however far along, however many steps — is not announced. After that, a
step that arrives already running, done, failed or skipped is news (an agent
that reveals each step as it starts it is the common stream), while one that
arrives
pendingis the plan growing and stays silent. - A failure skips the window, politely: it is spoken at once with its step, its reason and how many steps are still queued behind it.
- Reduced motion: the one animation is the running glyph's pulse, gated
with
motion-safe:. Under reduce the half-disc simply sits there, which is still a distinct shape. - Contrast: on a coloured plate every ink derives from the surface contract (§3), so a failure reads in the plate's error ink — which on the error, info and success plates is the plate's own ink, because those plates carry no second hue.
Keyboard and screen readers
- The list holds no tab stops of its own: Tab reaches only the controls you
put in
actions, in reading order. - Each step says its position and state before its label ("Step 3 of 5, failed: Write the tests"). The visible status word is not spoken again.
- When an action you pressed goes away, focus moves to that step's label, so the next thing read is the step's new state. If your handler already moved focus somewhere, it stays there.
- The progress bar is named "Plan progress" and reads "2 of 6 steps finished"; the text above it is not spoken twice.
- Step changes, a failure and the summary go through one polite status region. A summary that lands with the last step is one sentence with it.
In a narrow card
Labels wrap and keep their status word on the first line. Actions wrap under the failure.
AT matrix: NOT yet screen-reader tested (hence beta). NVDA and JAWS:
untested, no Windows host (QA §5.6).
Notes
- Vocabulary: this component says
doneandfailedwhereChatBubbleStepsayscompleteanderror. The chat component is shipped API and renaming it would be breaking for no gain, so the two words stand; pick the component by where it renders, not by the state names. - A running step is not a spinner. The default is a static half-disc with a
gentle pulse, because a plan is a page full of rows and a page full of
spinners is noise. Pass
icon={<Spinner label={null} />}on the step that should have one. - Failure does not change the steps behind it. They keep whatever status
you send. Whether a plan halts, skips the rest or carries on is the agent's
policy, and the component derives nothing — style it off
data-status.