GodUIGodUI
81

Anatomy of the Agent Timeline

How a live agent run is a spring-filled connector, three cross-faded ring states, and a collapsible body that morphs open on its own clock.

An agent run isn't a progress bar — it's a rail of independent decisions, each one reporting its own status as it lands. AgentTimeline renders that rail as an <ol> of AgentSteps, and every piece of motion in it is keyed off one prop: status.

The three pieces

Each AgentStep is a ring, a connector, and a body. The ring shows the step's current status. The connector below it fills as that status resolves, drawing the eye down to whatever's next. The body is optional — only steps with children get a chevron and something to expand.

Anatomyring · connector · body
Ring
border-color + fill keyed by status
Connector
scaleY 0 → 0.5 → 1, spring 320/32/0.9
Body
height + opacity, collapsible per step
tsx
 
<li data-status={status} className="relative flex gap-3 pb-2">
  <div className="relative flex flex-col items-center">
    <span className={STATUS_RING[status]}>{/* icon */}</span>
    {!last ? (
      <span className="relative my-1 w-px flex-1 overflow-hidden rounded bg-border">
        <motion.span className={`absolute inset-0 origin-top ${CONNECTOR_FILL[status]}`} />
      </span>
    ) : null}
  </div>
  <div className="min-w-0 flex-1 pb-2">{/* title, meta, chevron, body */}</div>
</li>

last is the only structural prop — it just hides the connector on the final step, so the rail doesn't dangle a line into nothing.

The connector: one number, three destinations

The connector is a motion.span inside an overflow-hidden track, scaled from an origin-top. scaleY maps directly off status: 0 while pending, 0.5 while running — a half-fill that reads as "in flight" — and 1 once the step lands on success or error. Every transition rides the same spring: stiffness: 320, damping: 32, mass: 0.9.

The motionpending → running → success, looping
Pending
hollow ring, dot
Running
spinner + ping, half fill
Success
solid ring, check, full fill
tsx
 
animate={{
  scaleY:
    status === "success" || status === "error"
      ? 1
      : status === "running"
        ? 0.5
        : 0,
}}
transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}

Why scaleY, not a growing height

A connector that grows by animating height has to re-run layout on every tick — the browser recalculates where every element below it sits, every frame. scaleY on a fixed-height track is a compositor transform: the browser scales an already-painted layer with no layout or paint work at all. The track's own height never changes; only the fill inside it stretches from a pinned top edge.

tsx
 
<span className="relative my-1 w-px flex-1 overflow-hidden rounded bg-border">
  <motion.span className="absolute inset-0 origin-top ..." /> {/* scaleY, not height */}
</span>

The ring's icon swap — dot → spinner → check — is a plain conditional render, not a cross-fade transition; only the connector and the collapsible body get the spring treatment. The running ring also grows a motion-reduce-gated animate-ping halo, layered absolutely over the same box so it never disturbs the ring's own layout.

The collapsible body

Steps with children get a chevron and an AnimatePresence-wrapped panel. Opening one is the same spring as the connector — height: "auto" and opacity animating together, so the panel never overshoots into visible overflow the way a plain duration tween would.

The bodycollapsed → open, looping
Body
scaleY + opacity, spring 320/32/0.9
Chevron
rotate 0 → 90deg, independent curve
tsx
 
<AnimatePresence initial={false}>
  {hasBody && open ? (
    <motion.div
      initial={{ height: 0, opacity: 0 }}
      animate={{ height: "auto", opacity: 1 }}
      exit={{ height: 0, opacity: 0 }}
      transition={{ type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}
      className="overflow-hidden"
    >
      {children}
    </motion.div>
  ) : null}
</AnimatePresence>

height: "auto" is the one place this component reaches for a layout animation instead of a transform — Motion measures the panel's natural height and interpolates a real pixel value under the hood, which is why the wrapper needs overflow-hidden to hide the in-between states. It's the right trade-off here: the panel's content is arbitrary (reasoning text, tool output), so there's no fixed size to fake with a transform, and it only happens on a deliberate click, not every frame of a running step.

The chevron is unrelated motion living on the same button — a plain rotate-90 toggle driven by open, with its own transition-transform, so it never has to wait on the spring to know which way it's pointing.

tsx
 
<ChevronIcon
  className={`transition-transform ${open ? "rotate-90" : ""}`}
/>

Cheap when idle, honest when running

The title pulses only while status === "running"motion-safe:animate-pulse scoped to that one class, so a step that's already resolved sits perfectly still. Nothing here polls or ticks on an interval; every step is a pure function of the status its caller passes in, so the whole timeline is exactly as busy as the run actually is.

tsx
 
className={`... ${status === "running" ? "text-muted-foreground motion-safe:animate-pulse" : ""}`}

Accessibility

The step header is a real <button>, disabled when there's no body to open — so keyboard users can Tab to it and toggle with Enter/Space for free, and screen readers get aria-expanded reflecting the live state. Every animate-spin, animate-ping, and motion-safe:animate-pulse in the rail carries a motion-reduce counterpart, and the connector/body springs both collapse to { duration: 0 } under useReducedMotion() — a run still reports its status instantly, just without the flourish.

tsx
 
const reduce = useReducedMotion();
transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 320, damping: 32, mass: 0.9 }}
tsx
 
<button
  type="button"
  disabled={!hasBody}
  aria-expanded={hasBody ? open : undefined}
  onClick={() => hasBody && setOpen((o) => !o)}
>

The result

A full run, advancing on its own — watch the connectors spring-fill as each step lands and the running step's body morph open.

Resultthe real component — advancing on its own
  1. Indexed 128 files across 6 packages.

One prop drives all of it: status in, a filled connector and a settled ring out. That's the whole timeline — a rail that never has to be told how to move, only what happened.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub