Task rows
NewAn agent’s tasks as they run, a row each: waiting, running, failed and retrying, or done, and the sub-steps behind them.
- Matched tax and contact IDs12/12
- Flagged stale records0
- Reading POS export3 files
- Scoring stockout risk68%
- Cone supplier follow-updraft
- Pistachio reorder notedraft
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- An agent’s run of a few tasks, shown as it happens: what is done, what is running, what failed and is retrying.
- Three to seven tasks, each a short title with a count of what it works on:
12 suppliers,7 SKUs. - Tasks worth opening: a row folds out onto its sub-steps, each with a figure, so the detail is there without crowding the run.
- A side panel or a chat turn, where the capsules read as a light checklist and the list as one compact card.
When not to use
- A single long job with a percentage. Use a progress bar, or the agent progress card.
- Dozens of tasks, or tasks to sort and filter. Use a table with a status column.
- Tasks the reader is meant to tick off by hand. Use a checklist with real checkboxes.
- The agent’s reasoning or its tool calls. Use the thinking trace or the tool chips: these rows are outcomes, not steps of thought.
You can add this component with the shadcn CLI or by hand:
Into components/task-rows of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/task-rows.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19.2 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { TaskRows } from './task-rows';export function RunStatus({ run }: { run: AgentRun }) { return ( <TaskRows tasks={run.tasks} defaultLayout="list" autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
The status of an agent’s run at a glance. Each task is a row: a check once it is done, a cross once it fails, its number in a turning ring while it runs. Then what it works on, a pill for the outcome, and a chevron that opens the row onto its sub-steps and their figures. Show the rows as capsules or as one list, with the switcher under them. On screen the run plays once: a task fails, retries, and completes.
- A row opens by animating grid-template-rows between 0fr and 1fr, the content fading along: 300 ms, with no height ever measured. Folded sub-steps are inert, so focus never lands in them.
- As a row opens its sub-steps rise 8 px and fade in, 100 ms apart; its chevron turns over in 300 ms, and a capsule squares off from a 22 px radius to 14 px.
- The running ring is two SVG circles, a track and an arc covering 28% of it, turning once every 1.1 s. A waiting task shows the same ring, still and without the arc.
- A new status swaps the icon with a 300 ms pop (a 0.95 scale and a fade) and fades its pill in over 200 ms; a failed pill spins a retry arrow every 1.2 s.
- The layouts are one list of rows styled two ways: capsules 8 px apart, or one card split by hairlines. Switching keeps what is open, and the rows rise in again 80 ms apart.
- While on screen, the run plays once: the running task starts open and the last one waits; 1.4 s later the running task folds and the last one fails; 1.4 s after that it completes. Any input of the reader’s ends it at once.
- Every colour is a light-dark() token on the root. Shadows that differ between the schemes are written once with all their layers, the layers a scheme does not use being transparent: a long soft shadow by day, a short drop at night.
| Prop | Type | Default | Description |
|---|---|---|---|
tasks | Task[] | a sample run | The tasks, a row each, in order: { id, title, count?, status: pending | running | failed | completed, steps?: { label, value }[] }. A task without steps does not open. |
layout | 'capsules' | 'list' | — | The layout, when the parent holds it; the switcher then hides. |
defaultLayout | 'capsules' | 'list' | 'capsules' | The layout it starts with, when it holds its own. |
onLayoutChange | (layout: TaskLayout) => void | — | Called when the switcher picks a layout. |
switcher | boolean | true | Shows the Capsules | List switcher under the rows. |
defaultOpen | string[] | [] | The ids of the rows open at first. |
onToggle | (id: string, open: boolean) => void | — | Called when a row opens or folds. |
autoplay | boolean | true | While on screen and untouched, the run plays once: the running task starts open, and the last task fails, retries and completes. Without it, the tasks are as given from the start. |
colorScheme | 'light' | 'dark' | 'system' | — | Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useRef, useState, type CSSProperties } from 'react';import { TASKS, type Task, type TaskStatus } from './data';import { useAutoplay } from './use-autoplay';import styles from './task-rows.module.css';export type { Task, TaskStatus, TaskStep } from './data';export type TaskLayout = 'capsules' | 'list';/* * Task rows: an agent's tasks as they run, one row each. A row reads its status at a glance: a * green check once completed, a red cross once failed (its pill then spins a retry arrow), its * number in a turning ring while running, and in a still ring while it waits. Then the task, * what it works on ("12 suppliers"), a status pill, and a chevron: the row opens onto its * sub-steps, each with its figure in monospace. * * - Two layouts, switched under the rows: capsules (a pill each, 8 px apart, which square off a * little while open) and a list (one card, rows split by hairlines). Switching keeps what is * open; the rows rise in again, 80 ms apart. * - A row opens by animating grid-template-rows between 0fr and 1fr, the content fading along: * no height is ever measured. Its sub-steps rise in 100 ms apart./* Task rows (task-rows.tsx): a soft grey card, and in its middle a column 440 px wide at most: the rows, as capsules or as a list, and under them the Capsules | List switcher. Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes the color-scheme it inherits from the page (light unless the page says otherwise), or the one its colorScheme prop sets (data-color-scheme). Shadows that differ between the schemes are written once with every layer, the layers a scheme does not use being transparent. Motion. Rows rise in by a transform, 80 ms apart; a row opens by animating grid-template-rows between 0fr and 1fr, so nothing is measured; a new status pops its icon in and fades its pill in. Under reduced motion all of it is instant and the rings stand still. */.root { /* The card, the rows on it, and a row under the pointer. */ --canvas: oklch(96.1% 0.002 247.84); --surface: oklch(100% 0 0); --inset: oklch(97.9% 0.002 247.839); /* The ink in three strengths, and the hairlines. */ --ink: oklch(24.7% 0.006 258.361); --ink-2: oklch(50.6% 0.01 264.477); --ink-3: oklch(69.5% 0.009 264.505); --line: oklch(94.6% 0.003 264.542); /* The switcher's track. */ --field: oklch(96.1% 0.001 286.375);// Copyright (c) 2026 Bruno Delorence. MIT License./* * What the rows show: the tasks of an agent's run, each with its status and the sub-steps it opens * onto. The sample is an ice-cream shop's agent, getting its suppliers and its reorders in order. */export type TaskStatus = 'pending' | 'running' | 'failed' | 'completed';export type TaskStep = { /** What the step did: "Reading POS export". */ label: string; /** Its figure, set in monospace on the right: "3 files", "68%", "draft". */ value: string;};export type Task = { id: string; /** The task, in a few words. */ title: string; /** What it works on, counted: "12 suppliers". */ count?: string; status: TaskStatus; /** The sub-steps the row opens onto; without them the row does not open. */'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver( ([entry]) => { onScreen = entry.isIntersecting;- The rows are a list; a row with sub-steps is a button with aria-expanded and aria-controls, and its folded sub-steps are inert, so they are neither focused nor read.
- Each row’s name carries its status in words (“Running”, “Failed, retrying”, “Completed”); the icons and the chevron are decorative (aria-hidden).
- Status changes are announced once through a polite live region, “Draft supplier emails: Completed.”
- The switcher is a labelled group of two buttons with aria-pressed. Focus shows a ring on every control.
- On narrow cards the counts are hidden from sight but still read.
- Under reduced motion the run never plays, rows open instantly, and the rings stand still.