Step List
SaveA compact checklist of an agent's plan on a connected rail, with queued, running, succeeded, failed, and skipped steps.
- Category
- Agent
- Version
- 0.2.0
- Frameworks
- Next.js, Vite + React
- Styling
- Tailwind CSS v4
Preview
Step List preview
Search the docs, Done
Found 3 pages
Edit the limits table, Running
Check links, Failed
2 links returned 404
Notify the team, Skipped
Open a pull request, Queued
Installation
npx @seamlessui/cli add step-listPre-release@seamlessui/cli is not on npm yet. Install from a local build.
Review the plan first. A dry run lists every file and package without changing anything:
npx @seamlessui/cli add step-list --dry-runUsage
step-list-example.tsx
import { StepList } from "@/components/seamless/agent/step-list";
export default function StepListExample() {
return (
<StepList
className="w-full max-w-sm"
steps={[
{ id: "1", label: "Search the docs", status: "succeeded", detail: "Found 3 pages" },
{ id: "2", label: "Edit the limits table", status: "running" },
{ id: "3", label: "Check links", status: "failed", detail: "2 links returned 404" },
{ id: "4", label: "Notify the team", status: "skipped" },
{ id: "5", label: "Open a pull request", status: "queued" },
]}
/>
);
}API
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | { id, label, status, detail?, meta? }[] | โ | Steps in order. detail shows a result or the error; meta is right-aligned metadata such as a time and duration. |
| StepStatusIcon | ({ status }) => JSX | โ | The status glyph on its own, also used by Run Timeline and the graph components. |
| StatusGlyph | ({ kind, size? }) => JSX | โ | The shared stroke glyphs: queued, running, success, failed, waiting, skipped. size is 16 or 20. |
Accessibility
- Each step has a visible text status and a screen-reader status after its label; glyphs are decorative.
- Skipped steps are struck through and labeled, not only dimmed.
- With reduced motion the spinner holds still and new steps, glyph swaps, and the check draw appear instantly.
Files
Installed into src/components/seamless, or components/seamless when the project has no src directory.
src/components/seamless/agent/step-list.tsxstep-list- Also installs agent-run@0.1.1registry dependency
- npm packages: noneReact and Tailwind CSS v4 required
Source
The exact source the CLI installs, from /r/items/react/step-list/0.2.0.json.
agent/step-list.tsxShowHide
agent/step-list.tsx
import type { ComponentProps, ReactNode } from "react";
import type { StepStatus } from "./agent-run";
const labels: Record<StepStatus, string> = {
queued: "Queued",
running: "Running",
succeeded: "Done",
failed: "Failed",
skipped: "Skipped",
};
// Only running and failed rows color their label; the glyph already carries the rest.
const labelTone: Record<StepStatus, string> = {
queued: "text-zinc-500",
running: "text-sky-600 dark:text-sky-400",
succeeded: "text-zinc-500",
failed: "text-red-600 dark:text-red-400",
skipped: "text-zinc-500",
};
export interface StepListItem {
id: string;
label: ReactNode;
status: StepStatus;
/** Secondary line, e.g. a result summary or the error. */
detail?: ReactNode;
/** Optional right-aligned metadata on the second line, e.g. a start time and duration. */
meta?: ReactNode;
}
export interface StepListProps extends Omit<ComponentProps<"ol">, "children"> {
steps: StepListItem[];
}
/**
* A compact checklist of an agent's plan. Steps sit on a connected rail; each shows a
* status glyph and a text status, and the rail turns green below finished steps.
*/
export function StepList({ steps, className, ...props }: StepListProps) {
return (
<ol className={["grid", className].filter(Boolean).join(" ")} {...props}>
{steps.map((step, index) => (
<li
key={step.id}
data-status={step.status}
className="relative grid grid-cols-[1rem_minmax(0,1fr)] gap-x-3 pb-4 transition-[opacity,translate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] last:pb-0 starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"
>
{index < steps.length - 1 ? (
<span
aria-hidden="true"
className={[
"absolute top-[1.375rem] bottom-0.5 left-[7.5px] w-px rounded-full transition-colors duration-200 motion-reduce:transition-none",
step.status === "succeeded" ? "bg-emerald-500/45" : "bg-zinc-950/10 dark:bg-white/12",
].join(" ")}
/>
) : null}
<StepStatusIcon key={step.status} status={step.status} className="mt-0.5" />
<div className="min-w-0">
<div className="flex items-baseline justify-between gap-3">
<p
className={[
"min-w-0 text-sm break-words",
step.status === "skipped"
? "text-zinc-500 line-through decoration-zinc-500/60"
: step.status === "queued"
? "text-zinc-600 dark:text-zinc-400"
: "text-zinc-950 dark:text-white",
step.status === "running" ? "font-medium" : "",
].join(" ")}
>
{step.label}
<span className="sr-only">, {labels[step.status]}</span>
</p>
<span
aria-hidden="true"
className={`shrink-0 text-xs transition-colors duration-200 motion-reduce:transition-none ${labelTone[step.status]}`}
>
{labels[step.status]}
</span>
</div>
{step.detail != null || step.meta != null ? (
<div className="mt-0.5 flex items-baseline justify-between gap-3">
{step.detail != null ? (
<p className={step.status === "failed" ? "min-w-0 text-xs text-red-600 dark:text-red-400" : "min-w-0 text-xs text-zinc-500"}>
{step.detail}
</p>
) : (
<span />
)}
{step.meta != null ? <span className="shrink-0 font-mono text-[11px] text-zinc-500 tabular-nums">{step.meta}</span> : null}
</div>
) : null}
</div>
</li>
))}
</ol>
);
}
/** The shared status shapes. Each one is paired with a text label wherever it is used. */
export type StatusGlyphKind = "queued" | "running" | "success" | "failed" | "waiting" | "skipped";
const glyphTone: Record<StatusGlyphKind, string> = {
queued: "text-zinc-500",
running: "text-sky-600 dark:text-sky-400",
success: "text-emerald-600 dark:text-emerald-400",
failed: "text-red-600 dark:text-red-400",
waiting: "text-amber-600 dark:text-amber-400",
skipped: "text-zinc-500",
};
const stepGlyph: Record<StepStatus, StatusGlyphKind> = {
queued: "queued",
running: "running",
succeeded: "success",
failed: "failed",
skipped: "skipped",
};
/**
* A 16px or 20px stroke glyph for an agent state. Remount it with `key={kind}` so the new
* shape scales in (and the success check draws); both are instant with reduced motion.
* The running arc is the only loop, and it holds still with reduced motion.
*/
export function StatusGlyph({ kind, size = 16, className = "" }: { kind: StatusGlyphKind; size?: 16 | 20; className?: string }) {
return (
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
// 1.2 in a 16 unit box drawn at 20px is a 1.5px stroke, matching the 16px glyph.
strokeWidth={size === 20 ? 1.2 : 1.5}
strokeLinecap="round"
strokeLinejoin="round"
data-glyph={kind}
className={[
size === 20 ? "size-5" : "size-4",
"shrink-0 transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] starting:scale-90 starting:opacity-0 motion-reduce:transition-none",
glyphTone[kind],
className,
].join(" ")}
>
{kind === "queued" ? (
<circle cx="8" cy="8" r="6.25" strokeDasharray="2.15 2.75" />
) : kind === "running" ? (
<>
<circle cx="8" cy="8" r="6.25" strokeOpacity="0.2" />
<path d="M8 1.75A6.25 6.25 0 1 1 1.75 8" className="origin-center motion-safe:animate-spin" />
</>
) : (
<>
<circle cx="8" cy="8" r="6.25" />
{kind === "success" ? (
<path
d="m5.35 8.25 1.75 1.75 3.55-3.75"
pathLength={1}
strokeDasharray="1"
className="[stroke-dashoffset:0] transition-[stroke-dashoffset] delay-75 duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] starting:[stroke-dashoffset:1] motion-reduce:transition-none"
/>
) : kind === "failed" ? (
<path d="m6.1 6.1 3.8 3.8m0-3.8-3.8 3.8" />
) : kind === "waiting" ? (
<path d="M6.6 5.9v4.2M9.4 5.9v4.2" />
) : (
<path d="m5.6 10.4 4.8-4.8" />
)}
</>
)}
</svg>
);
}
/** Status glyph for steps; remount with `key={status}` to play its entry transition. */
export function StepStatusIcon({ status, className = "" }: { status: StepStatus; className?: string }) {
return <StatusGlyph kind={stepGlyph[status]} className={className} />;
}