Loaders, AI & Widgetsv0.1.0
Agent Progress Steps
Steps move from loading to done with drawn checks.
Installation
CLI
npx shadcn@latest add https://hantui-theta.vercel.app/r/agent-steps.jsonManual
Install the dependencies
npm install clsx motion tailwind-mergeAdd the shared helpers
Components use
cn()fromlib/utils.ts. See Installation if you have not set them up yet.Add the theme tokens
This component uses --destructive-text and --success, which shadcn does not define by default. Add them to your global CSS (--destructive-text falls back to your --destructive unless you set it):
app/globals.css:root { --success: #15803d; } .dark { --success: #4ade80; } @theme inline { --color-destructive-text: var(--destructive-text, var(--destructive)); --color-success: var(--success); }Copy the source
components/ui/agent-steps/agent-steps.tsx"use client"; import type { ComponentProps, ReactNode } from "react"; import { motion, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; import { ShimmerText } from "@/components/ui/shimmer-text/shimmer-text"; import { SpinnerCheck, type SpinnerStatus } from "@/components/ui/spinner-check/spinner-check"; export type AgentStepStatus = "pending" | "running" | "done" | "error"; export interface AgentStep { /** Stable identity. */ id: string; /** What the step does, e.g. "Reading 4 files". */ title: string; /** Detail under the title, e.g. a file name or result. */ description?: ReactNode; /** Where the step is. */ status: AgentStepStatus; } export interface AgentStepsProps extends Omit<ComponentProps<"ol">, "children" | "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd"> { /** Steps in order. */ steps: AgentStep[]; } const ICON: Record<AgentStepStatus, SpinnerStatus> = { pending: "idle", running: "loading", done: "success", error: "error" }; const SPOKEN: Record<AgentStepStatus, string> = { pending: "pending", running: "in progress", done: "completed", error: "failed" }; export function AgentSteps({ steps, className, "aria-label": ariaLabel = "Progress", ...props }: AgentStepsProps) { const reduceMotion = useReducedMotion(); return ( <ol aria-label={ariaLabel} className={cn("relative", className)} {...props}> {steps.map((step, i) => { const last = i === steps.length - 1; return ( <li key={step.id} aria-current={step.status === "running" ? "step" : undefined} className="relative flex gap-3 pb-5 last:pb-0"> {/* The connector to the next step fills in once this step is done. */} {!last ? ( <span aria-hidden="true" className="absolute top-6 bottom-1 left-[9px] w-0.5 overflow-hidden rounded-full bg-border"> <motion.span className="block size-full origin-top bg-success" initial={false} animate={{ scaleY: step.status === "done" ? 1 : 0 }} transition={reduceMotion ? { duration: 0 } : { duration: 0.45, ease: [0.22, 1, 0.36, 1], delay: 0.15 }} /> </span> ) : null} <SpinnerCheck aria-hidden="true" role="presentation" status={ICON[step.status]} size={20} className="mt-px text-muted-foreground" /> <div className="min-w-0 flex-1"> <p className={cn( "text-sm font-medium", step.status === "pending" && "text-muted-foreground", step.status === "done" && "text-foreground", step.status === "error" && "text-destructive-text", )} > {step.status === "running" ? <ShimmerText>{step.title}</ShimmerText> : step.title} <span className="sr-only">, {SPOKEN[step.status]}</span> </p> {step.description ? ( <div className="mt-0.5 text-xs leading-5 text-muted-foreground">{step.description}</div> ) : null} </div> </li> ); })} </ol> ); }
Usage
import { AgentSteps } from "@/components/ui/agent-steps/agent-steps";
<AgentSteps
steps={[
{ id: "read", title: "Reading the issue", status: "done" },
{ id: "fix", title: "Writing a fix", status: "running", description: "pricing/total.ts" },
{ id: "test", title: "Running the tests", status: "pending" },
]}
/>API reference
| Prop | Type | Default | Description |
|---|---|---|---|
steps* | AgentStep[] | – | Steps in order. |
Also accepts every native <ol> attribute, including ref and className.
- Each step's icon moves from a hollow ring to a spinner to a drawn check (or a cross), and the connector to the next step fills once it's done.
- The running step's title shimmers. Built from Spinner to Check and Shimmer Text, installed along with it.
Accessibility
- An ordered list; the running step has aria-current="step" and every step's status is spelled out for screen readers (completed, in progress, pending, failed).
- With reduced motion enabled, connectors fill instantly and checks appear without drawing.