- 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
Working
Send a message to begin exploring.
Installation
Set up the @cobalt registry as described in Installation, then add the block:
$ pnpm dlx shadcn@latest add @cobalt/web-search-01
Usage
Pass steps with a label, optional result count, and optional sources. Each source accepts a title, domain, and optional link. Set dwell in milliseconds to control the delay before a step appears. Pass revealed to control the number of visible rows externally; source rows count separately. Set working={false} to hide the progress indicator, and use onComplete to respond when all rows are visible.
Source
"use client";
import { RiGlobeLine, RiSearchLine, RiSparklingLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Trace,
TraceDescription,
TraceIcon,
TraceLine,
TraceTitle,
} from "@/components/ui/trace";
export interface WebSearchSource {
title: string;
domain: string;
href?: string;
}
export interface WebSearchStep {
label: string;
meta?: string;
sources?: WebSearchSource[];
dwell?: number;
}
/** A compact, progressively revealed research trail. */
export function WebSearch({
steps,
revealed,
working = "Searching",
onComplete,
className,
}: {
steps: WebSearchStep[];
/** Number of visible rows, including source rows. Omit to use the timer. */
revealed?: number;
working?: string | false;
onComplete?: () => void;
className?: string;
}) {
const rows = React.useMemo(
() =>
steps.flatMap((step, index) => [
{ kind: "step" as const, step, index },
...(step.sources?.length
? [{ kind: "sources" as const, step, index }]
: []),
]),
[steps]
);
const [count, setCount] = React.useState(0);
const completed = React.useRef(false);
React.useEffect(() => {
if (revealed !== undefined || count >= rows.length) {
return;
}
const row = rows[count];
const timer = window.setTimeout(
() => setCount((value) => value + 1),
row.kind === "step" ? (row.step.dwell ?? 700) : 250
);
return () => window.clearTimeout(timer);
}, [count, revealed, rows]);
const visible = Math.max(0, Math.min(revealed ?? count, rows.length));
React.useEffect(() => {
if (visible >= rows.length && !completed.current) {
completed.current = true;
onComplete?.();
} else if (visible < rows.length) {
completed.current = false;
}
}, [visible, rows.length, onComplete]);
return (
<Trace
className={cn("w-fit max-w-full gap-0", className)}
aria-live="polite"
>
{rows.slice(0, visible).map((row, rowIndex) =>
row.kind === "step" ? (
<div
key={`step-${row.index}`}
style={{ "--delay": `${rowIndex * 40}ms` } as React.CSSProperties}
className={cn(
"web-search-01-row-in relative flex min-h-7 min-w-0 items-start",
row.index > 0 && "ml-2.5 border-l border-border pl-4"
)}
>
{row.index > 0 && (
<span
aria-hidden
className="absolute top-0 -left-px h-3 w-3 rounded-bl-lg border-b border-l border-border"
/>
)}
<TraceLine>
<TraceIcon>
<RiSearchLine />
</TraceIcon>
<TraceTitle className="whitespace-normal wrap-anywhere">
{row.step.label}
</TraceTitle>
{row.step.meta && (
<TraceDescription className="ml-2 hidden @[240px]:inline">
{row.step.meta}
</TraceDescription>
)}
</TraceLine>
</div>
) : (
<div
key={`sources-${row.index}`}
style={{ "--delay": `${rowIndex * 40}ms` } as React.CSSProperties}
className="relative ml-2.5 flex min-h-7 min-w-0 items-start border-l border-border pl-4 web-search-01-row-in"
>
<span
aria-hidden
className="absolute top-0 -left-px h-3 w-3 rounded-bl-lg border-b border-l border-border"
/>
<div className="flex min-w-0 flex-wrap items-center gap-2 text-sm leading-5 text-muted-foreground">
<span>Sources</span>
<span className="flex flex-wrap items-center gap-1">
{row.step.sources?.slice(0, 6).map((source, sourceIndex) => {
const mark = (
<Avatar
size="xs"
title={`${source.title} — ${source.domain}`}
>
<AvatarFallback>
{source.domain.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
);
return source.href ? (
<a
key={`${source.domain}-${sourceIndex}`}
href={source.href}
aria-label={`${source.title} — ${source.domain}`}
className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{mark}
</a>
) : (
<span
key={`${source.domain}-${sourceIndex}`}
aria-label={`${source.title} — ${source.domain}`}
>
{mark}
</span>
);
})}
{(row.step.sources?.length ?? 0) > 6 && (
<span className="flex h-5 min-w-5 items-center justify-center rounded-full border border-background bg-muted px-1 text-xs">
+{(row.step.sources?.length ?? 0) - 6}
</span>
)}
</span>
</div>
</div>
)
)}
{visible < rows.length && working && (
<TraceLine status="running" className="mt-1">
<TraceIcon>
<RiSparklingLine />
</TraceIcon>
<TraceTitle>{working}</TraceTitle>
</TraceLine>
)}
{rows.length === 0 && (
<TraceLine>
<TraceIcon>
<RiGlobeLine />
</TraceIcon>
<TraceTitle>No searches yet</TraceTitle>
</TraceLine>
)}
</Trace>
);
} On this page