DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. PlanList

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/ui

The 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, failed or skipped independently 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

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
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.

  • detailed is a plan card's rows. It has a wide glyph column, the step's activity lines while it runs or after it fails (they fold away once it is done), a failure block with its controls, the duration at the end, and a note line naming who added a step.
  • compact is one centred line per step, inked by state: the running step in full ink, a failed one in the error ink, the rest muted.
  • activity leads each row with its tool glyph and carries the state in the label's ink, with the duration trailing. Set collapsed on 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

PropTypeDefaultDescription
stepsPlanStep[]—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"inheritedInherited from a wrapping <Surface>, else "default"; an explicit prop wins
dirstring—Text direction; "rtl" also picks the Arabic strings
stringsPartial<PlanListStrings>—Per-entry string overrides
showStatusbooleanfalseShows each step's state as a word at the row's end. Not spoken — the row's hidden prefix already says it
showProgressbooleanfalseShows "2 of 6 steps finished" over a thin bar above the steps. Counts top-level done and skipped
summaryReactNode—A closing line under the steps. Announced once when it appears after the first paint
summaryTextstringthe summary when it is a stringPlain text of summary for the announcement
classNamestring—Additional classes on the root wrapper

aria-* and role land on the <ol>; everything else lands on the root.

PlanStep

FieldTypeDefaultDescription
idstring—Stable identity across renders — the key the announcer diffs on
labelReactNode—The visible step label
labelTextstringthe label when it is a stringPlain text for announcements; required when label is a node
statusPlanStepStatus"pending"pending · running · done · failed · skipped
resultReactNode—Result line under the label — a count, a file, a duration
errorReactNode—Why a step failed; a string is also read out in the announcement
iconReactNode—Overrides the status glyph — e.g. <Spinner label={null} />
substepsPlanStep[]—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
durationReactNode—Trailing time, at the row's end in detailed and activity rows
noteReactNode—One line under the label naming who added the step. detailed rows
insertedbooleanfalseThe step was added after the plan was written: it arrives on a wash that fades
collapsedbooleanfalseFolds the row away and hides it — an older step scrolled out of a short list. activity rows
actionsReactNode—Controls under the step — Retry and Skip on a failed one. If the focused one leaves, focus moves to the step's label

PlanListSkeleton

PropTypeDefaultDescription
itemsnumber3Placeholder rows
dirstring—Text direction
labelstring"Loading plan", Arabic under dir="rtl"What AT announces while the placeholder is up — rendered as sr-only text, not aria-label
showProgressbooleanfalseReserves the progress line, matching PlanList showProgress
showStatusbooleanfalseReserves each row's status word, matching PlanList showStatus

DOM contract

AttributeWhereValues
data-slotevery partplan-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-statusevery <li>pending · running · done · failed · skipped
aria-currentthe running <li>step

What it's made of

PartWhat it is
StepsAn ordered list; sub-steps a nested ordered list
Status glyphFive 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 wordText at the row's end with showStatus
Result and failure linesText under the label; a failure in the failure ink
ActionsYour controls — usually two Buttons, xs
Progress lineA line of text over a Progress bar, xs
SummaryText 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-only prefix — "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 an aria-label on the <li>: a listitem takes 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-status carries the same fact to the DOM. Glyphs are aria-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 pending is 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 done and failed where ChatBubbleStep says complete and error. 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.
ToolCallParameterDiff

On this page

When to useWhen not to useInstallationUsageExamplesPlaygroundDefaultStatesSub-stepsFailureVariantsProgress and summaryStreamingIn a narrow cardOn colorRight to leftLoadingAPI ReferencePlanListPlanStepPlanListSkeletonDOM contractWhat it's made ofStatesMotionAccessibilityKeyboard and screen readersIn a narrow cardNotes