## Web Search (S—484)

An AI agent’s web research as a live timeline: searches, pages opened, sources as overlapping site marks, errors and retry.

- URL: https://sundaeui.dev/components/web-search
- Category: UI components · Art direction: Refined
- Access: Sundae Pro (authenticated install)
- Stack: React 18+, TypeScript, plain CSS; dependencies: none
- Install: `npx shadcn@latest add https://sundaeui.dev/r/web-search.json`
- Tags: tool-calls, web-search, timeline, agent, llm, sources, streaming, research, ai, citations

Shows where an agent went while it researched. Each search appears with its query and, when it lands, the result count and the sources it kept, drawn as overlapping site marks that expand into a list of linked pages with titles and snippets. Pages it opened appear as reads, with an optional one-line takeaway.
While the agent works, the header shimmers with what it is doing right now and a live timer; running steps spin and shimmer, the rail draws itself down as new steps arrive, and a failed search turns red with the reason and a retry button. When research ends, the header summarises it (“Researched 3 searches, 2 pages · 9 sites”) and can collapse to that line.
Everything comes from props: append steps and update their status from your agent’s tool events. Favicons are optional; without them each site gets a lettered mark in a stable colour.

### Usage

```tsx
import { WebSearch, type ResearchStep } from '@/components/sundae/web-search'

// Build steps from your agent's tool events as they stream in.
const [steps, setSteps] = useState<ResearchStep[]>([])

onToolStart((call) => setSteps((s) => [...s, call.name === 'web_search'
  ? { id: call.id, kind: 'search', query: call.input.query, status: 'running' }
  : { id: call.id, kind: 'read', url: call.input.url, status: 'running' }]))

onToolResult((r) => setSteps((s) => s.map((x) => x.id === r.id
  ? { ...x, status: 'done', resultCount: r.total, sources: r.results.map((h) => ({ url: h.url, title: h.title, snippet: h.snippet })) }
  : x)))

<WebSearch steps={steps} status={done ? 'done' : 'running'} startedAt={startedAt} endedAt={endedAt} onRetry={retry} />
```

### Props

- `steps` (`ResearchStep[]`, required): Searches ({ kind: 'search', query, resultCount, sources }) and reads ({ kind: 'read', url, title, summary }), each with id and status: pending, running, done or error.
- `status` (`'running' | 'done' | 'error'`, required): Overall state: running shows the shimmer and live timer.
- `startedAt` (`number`): Date.now() when research began; drives the timer.
- `endedAt` (`number`): When it finished, for the final duration.
- `onRetry` (`(stepId: string) => void`): Shows Retry on failed steps.
- `collapseOnDone` (`boolean`, default `false`): Collapse to the summary line when finished.
- `defaultOpen` (`boolean`, default `true`): Start expanded.
- `maxMarks` (`number`, default `4`): Site marks shown per search before “+N”.
- `theme` (`'auto' | 'light' | 'dark'`, default `'auto'`): auto follows a .dark ancestor.

### When to use

- Agents and assistants that search the web or a knowledge base before answering.
- Making long research tasks feel transparent and checkable.

### When not to use

- Showing the reasoning itself: use Reasoning Trace.
- A single search box with results: use Search Field.

### Accessibility

- The header and each “N sources” row are buttons with aria-expanded controlling their panels; collapsed panels are inert.
- A polite live region announces what the agent is doing now (“Searching for …”, “Reading …”) and the final summary, not every update.
- Source links say they open in a new tab.
- Reduced motion removes shimmer, spinners, the drawing rail and entrances.
