Agent Steps
A live tool-call timeline for agent runs: shimmer on the active step, ticks on finished ones, expandable details.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/agent-steps.json'use client'
import { useEffect, useId, useState } from 'react'
import './agent-steps.css'
export interface AgentStep {
title: string
detail?: string
/** Seconds the step takes in the demo timeline. */
seconds: number
}
export interface AgentStepsProps {
steps: AgentStep[]
/** Index of the step running now; steps before it are done. steps.length means all done. */
current: number
title?: string
density?: 'comfy' | 'compact'
fill?: 'dark' | 'light'
className?: string
}
/** A live tool-call timeline for agent runs: spinner on the active step, ticks on finished ones, expandable details. */
export function AgentSteps({ steps, current, title = 'Working', density = 'comfy', fill = 'dark', className }: AgentStepsProps) {
const [open, setOpen] = useState<number | null>(null)
const id = useId()
const done = current >= steps.length
useEffect(() => setOpen(null), [steps])
return (
<section className={['sd-as', className].filter(Boolean).join(' ')} data-fill={fill} data-density={density} aria-labelledby={`${id}-t`}>
<header className="sd-as__head">
<span className="sd-as__badge" data-done={done || undefined} aria-hidden />
<h3 id={`${id}-t`}>{done ? 'Done' : title}</h3>
<span className="sd-as__count" aria-live="polite">
{Math.min(current, steps.length)}/{steps.length} steps
</span>
</header>
<ol className="sd-as__list">
{steps.map((s, i) => {
const st = i < current ? 'done' : i === current ? 'run' : 'todo'
return (
<li key={s.title} data-st={st}>
<span className="sd-as__icon" aria-hidden>
{st === 'done' ? <svg viewBox="0 0 16 16"><path d="m4 8.5 2.5 2.5L12 5.5" /></svg> : null}
</span>
<div className="sd-as__body">
<button type="button" className="sd-as__title" aria-expanded={open === i} aria-controls={`${id}-${i}`} disabled={!s.detail} onClick={() => setOpen(open === i ? null : i)}>
{s.title}
<span className="sd-sr">{st === 'done' ? ' (done)' : st === 'run' ? ' (running)' : ' (pending)'}</span>
{st !== 'todo' && <span className="sd-as__time">{s.seconds}s</span>}
</button>
<div id={`${id}-${i}`} className="sd-as__detail" data-open={open === i || undefined}>
<div><p>{s.detail}</p></div>
</div>
</div>
</li>
)
})}
</ol>
</section>
)
}.sd-as { --bg: #1d1d1d; --fg: #f7f7f7; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --ok: #34d399; --run: #818cf8; --code: rgb(255 255 255 / 0.06);
width: min(100%, 310px); padding: 16px; border-radius: 22px; background: var(--bg); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-as[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.08); --ok: #059669; --run: #4f46e5; --code: rgb(0 0 0 / 0.05); }
.sd-as__head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.sd-as__head h3 { margin: 0; font-size: 15px; font-weight: 600; }
.sd-as__badge { width: 9px; height: 9px; border-radius: 99px; background: var(--run); box-shadow: 0 0 0 4px color-mix(in srgb, var(--run) 25%, transparent); animation: sd-as-pulse 1.2s ease-in-out infinite; }
.sd-as__badge[data-done] { background: var(--ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 25%, transparent); animation: none; }
@keyframes sd-as-pulse { 50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--run) 0%, transparent); } }
.sd-as__count { margin-left: auto; font-size: 12px; color: var(--mute); font-feature-settings: 'tnum'; }
.sd-as__list { margin: 0; padding: 0; list-style: none; }
.sd-as__list li { position: relative; display: flex; gap: 10px; padding-bottom: 10px; }
.sd-as[data-density='compact'] .sd-as__list li { padding-bottom: 4px; }
.sd-as__list li:not(:last-child)::before { content: ''; position: absolute; left: 9px; top: 22px; bottom: 0; width: 2px; border-radius: 2px; background: var(--line); }
.sd-as__list li[data-st='done']:not(:last-child)::before { background: color-mix(in srgb, var(--ok) 50%, transparent); }
.sd-as__icon { flex-shrink: 0; display: grid; place-items: center; width: 20px; height: 20px; margin-top: 1px; border-radius: 99px; box-shadow: inset 0 0 0 1.5px var(--line); transition: background-color 300ms; }
li[data-st='done'] .sd-as__icon { background: var(--ok); box-shadow: none; }
li[data-st='done'] .sd-as__icon svg { width: 12px; fill: none; stroke: #0b0b0b; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 14; animation: sd-as-draw 400ms ease-out; }
@keyframes sd-as-draw { from { stroke-dashoffset: 14; } }
li[data-st='run'] .sd-as__icon { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--run) 25%, transparent); border-top: 2px solid var(--run); animation: sd-as-spin 800ms linear infinite; }
@keyframes sd-as-spin { to { rotate: 360deg; } }
.sd-as__body { min-width: 0; flex: 1; }
.sd-as__title { display: flex; width: 100%; align-items: baseline; gap: 6px; text-align: left; font-size: 14px; color: var(--fg); }
li[data-st='todo'] .sd-as__title { color: var(--mute); }
li[data-st='run'] .sd-as__title { color: var(--fg); font-weight: 500; }
@keyframes sd-as-shine { to { background-position: -200% 0; } }
.sd-as__title:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; border-radius: 4px; }
.sd-as__time { margin-left: auto; font-size: 12px; color: var(--mute); font-feature-settings: 'tnum'; }
.sd-as__detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 350ms cubic-bezier(0.2, 1, 0.3, 1); }
.sd-as__detail[data-open] { grid-template-rows: 1fr; }
.sd-as__detail > div { overflow: hidden; }
.sd-as__detail p { margin: 6px 0 2px; padding: 8px 10px; border-radius: 10px; background: var(--code); font: 12px/1.5 ui-monospace, monospace; color: var(--mute); }
.sd-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .sd-as *, .sd-as__badge { animation: none !important; transition: none !important; } li[data-st='run'] .sd-as__title { color: var(--fg); background: none; } }Overview
Shows what an agent is doing while it works. The running step shimmers with a spinner; finished steps draw a tick and show their time.
Each step can carry a detail line (a query, files read, test results) that expands in place.
When to use
- Agent and copilot UIs that run multi-step tool calls.
- Long background jobs with a few named phases.
When not to use
- Single quick actions: a spinner is enough.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/agent-steps.json$
npx shadcn@latest add https://sundaeui.dev/r/agent-steps.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/agent-steps.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/agent-steps.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy agent-steps.tsx and agent-steps.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { AgentSteps } from '@/components/sundae/agent-steps'
<AgentSteps
current={2}
steps={[
{ title: 'Searched the docs', detail: '6 results', seconds: 2 },
{ title: 'Read 3 files', seconds: 4 },
{ title: 'Drafted the patch', seconds: 6 },
]}
/>Props
| Prop | Type | Default |
|---|---|---|
steps*Title, optional detail and seconds. | AgentStep[] | — |
current*Index of the running step; steps.length when done. | number | — |
titleHeading while running. | string | 'Working' |
densityRow spacing. | 'comfy' | 'compact' | 'comfy' |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- An ordered list; each step’s state is in visually hidden text ("running", "done").
- Details toggle with aria-expanded/aria-controls.
- The step count is a polite live region. Reduced motion removes spinners and shimmer.
Customization
- Every colour is a CSS custom property on the root class; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- Tab
- Move between steps
- Enter / Space
- Show or hide details
FAQ
- Is Agent Steps free?
- Yes. Agent Steps is free for personal and commercial projects, with full source on this page.
- How do I install Agent Steps?
- Run npx shadcn@latest add https://sundaeui.dev/r/agent-steps.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Agent Steps support keyboard and screen readers?
- An ordered list; each step’s state is in visually hidden text ("running", "done"). Details toggle with aria-expanded/aria-controls.
- Does Agent Steps respect reduced motion?
- The step count is a polite live region. Reduced motion removes spinners and shimmer.
More like this
All components →Context Meter
Components · Futuristic- Pro
Reasoning Trace
Components · Refined - Pro
AI Composer
Components · Understated - Pro
Voice Orb
Components · Futuristic Setup Checklist
Components · UnderstatedFold Accordion
Components · Understated