- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Installation
Set up the @cobalt registry first, as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/task-list-01
Usage
Provide tasks with a title and steps. Each step can include resource labels and optional icons. The list reveals one heading or step at a time; use startDelay and stepInterval to change the timing. Set collapseOnComplete to true to fold each finished task, or to "all" to fold them when the entire run finishes. Headings remain interactive so readers can reopen them.
For live events, pass revealed as the number of headings and steps to show. onComplete fires when all units have been revealed. Remount the list with a new React key to start a fresh timed run.
Source
"use client";
import {
RiArrowDownSLine,
RiSearchLine,
RiSparklingLine,
} from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
export interface TaskListStep {
label: string;
resources?: { label: string; icon?: React.ReactNode }[];
}
export interface TaskListTask {
title: string;
steps: TaskListStep[];
icon?: React.ReactNode;
}
export interface TaskListProps {
tasks: TaskListTask[];
startDelay?: number;
stepInterval?: number;
revealed?: number;
collapseOnComplete?: boolean | "all";
onComplete?: () => void;
className?: string;
}
/** Each task heading and each step occupies one unit of progress. */
export function TaskList({
tasks,
startDelay = 320,
stepInterval = 850,
revealed,
collapseOnComplete = false,
onComplete,
className,
}: TaskListProps) {
const total = tasks.reduce((count, task) => count + 1 + task.steps.length, 0);
const [internalProgress, setInternalProgress] = React.useState(0);
const [expanded, setExpanded] = React.useState<Record<number, boolean>>({});
const [elapsed, setElapsed] = React.useState(0);
const completedRef = React.useRef(false);
const progress = Math.max(0, Math.min(total, revealed ?? internalProgress));
const complete = progress >= total;
React.useEffect(() => {
if (revealed !== undefined || internalProgress >= total) {
return;
}
const timeout = window.setTimeout(
() => setInternalProgress((value) => Math.min(value + 1, total)),
internalProgress === 0
? Math.max(0, startDelay)
: Math.max(0, stepInterval)
);
return () => window.clearTimeout(timeout);
}, [internalProgress, revealed, startDelay, stepInterval, total]);
React.useEffect(() => {
if (complete) {
return;
}
const started = Date.now();
const timer = window.setInterval(
() => setElapsed((Date.now() - started) / 1000),
100
);
return () => window.clearInterval(timer);
}, [complete]);
React.useEffect(() => {
if (complete && !completedRef.current) {
completedRef.current = true;
onComplete?.();
} else if (!complete) {
completedRef.current = false;
}
}, [complete, onComplete]);
let offset = 0;
return (
<div
className={cn("flex min-w-0 flex-col gap-3 text-sm leading-5", className)}
>
{tasks.map((task, index) => {
const start = offset;
offset += 1 + task.steps.length;
if (progress <= start) {
return null;
}
const visibleSteps = Math.min(task.steps.length, progress - start - 1);
const taskComplete = visibleSteps === task.steps.length;
const autoCollapsed =
collapseOnComplete === "all"
? complete
: collapseOnComplete && taskComplete;
const open = expanded[index] ?? !autoCollapsed;
return (
<div key={index} className="task-list-01-enter min-w-0">
<Collapsible
open={open}
onOpenChange={(next) =>
setExpanded((previous) => ({ ...previous, [index]: next }))
}
>
<CollapsibleTrigger className="w-full cursor-pointer text-left">
<span className="flex min-w-0 items-center gap-2 rounded-[4px]">
<span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
{task.icon ?? <RiSearchLine className="size-4" />}
</span>
<span className="min-w-0 flex-1 break-words">
{task.title}
</span>
<RiArrowDownSLine
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform duration-150 motion-reduce:transition-none",
open && "rotate-180"
)}
/>
</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="pt-1">
{visibleSteps > 0 && (
<div className="ml-1.75 flex flex-col border-l border-border pl-4">
{task.steps
.slice(0, visibleSteps)
.map((step, stepIndex) => (
<div
key={stepIndex}
className="relative flex min-h-6 min-w-0 flex-wrap items-center gap-1.5 text-muted-foreground task-list-01-enter-delayed"
style={
{
"--delay": `${stepIndex * 40}ms`,
} as React.CSSProperties
}
>
<span className="absolute top-0 -left-4 h-3 w-3 rounded-bl-md border-b border-l border-border" />
<span className="min-w-0 max-w-full break-words">
{step.label}
</span>
{step.resources?.map((resource, resourceIndex) => (
<Badge
key={resourceIndex}
variant="counter"
tone="neutral"
className="max-w-full"
>
{resource.icon && (
<span className="mr-1 shrink-0">
{resource.icon}
</span>
)}
<span className="truncate">
{resource.label}
</span>
</Badge>
))}
</div>
))}
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
})}
{!complete && progress > 0 && (
<div
role="status"
className="flex items-center gap-2 text-xs text-muted-foreground"
>
<RiSparklingLine className="size-4" />
<span>Working</span>
<span>{elapsed.toFixed(1)}s</span>
</div>
)}
</div>
);
} On this page