37 / 122 · Knowledge
Web search
A search query and its results landing one by one as the agent reads.
assistant-ui draft persistence
Searching
Installation
npx shadcn@latest add "@assistant-ui/elements-web-search"
Usage
import { WebSearch } from "@/components/elements/web-search";
<WebSearch
query="assistant-ui composer"
results={[{ title: "Composer guide", domain: "docs.example.com" }]}
visibleResults={1}
searching={false}
cycle={0}
/>Props
query*stringSearch query shown in the query pill.
results*readonly WebSearchResult[]Result cards available to reveal one by one.
visibleResults*numberHow many results from the start are currently shown.
searching*booleanWhen true a shimmering Searching label replaces the result count.
cycle*numberIdentity key that remounts the list so entrance animation can replay.
classNamestringExtra classes merged onto the root.
This element normalizes its counts and shares at the boundary, so a value from outside its range reads as the nearest end rather than reaching the DOM: a negative count shows nothing, one past the end shows everything, and a share stays within 0 to 100 percent. A prop that names a selection is the exception, and means nothing is selected.
Source
web-search.tsx"use client";
import type { ComponentProps } from "react";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, mono, ShimmerLabel } from "./surfaces";
import { take } from "./range";
export interface WebSearchResult {
title: string;
domain: string;
}
export function WebSearch({
query,
results,
visibleResults,
searching,
cycle,
className,
...props
}: Omit<
ComponentProps<"div">,
"children" | "query" | "results" | "visibleResults" | "searching" | "cycle"
> & {
query: string;
results: readonly WebSearchResult[];
visibleResults: number;
searching: boolean;
cycle: number;
}) {
return (
<div
data-slot="web-search"
className={cn("flex w-full max-w-sm flex-col gap-2.5", className)}
{...props}
>
<span
className={cn(
field,
"text-foreground/70 inline-flex w-fit items-center gap-1.5 rounded-full px-3.5 py-2 text-xs",
)}
>
<SearchIcon className="text-foreground/40 size-3" />
{query}
</span>
<div className="text-foreground/45 text-xs">
{searching ? (
<ShimmerLabel className="relative inline-block leading-none">
Searching
</ShimmerLabel>
) : (
<span className="fade-in animate-in duration-300">
Read 3 sources
</span>
)}
</div>
<div className="flex min-h-[5.75rem] flex-col">
{take(results, visibleResults).map((result) => (
<div
key={`${cycle}-${result.domain}`}
className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both hover:bg-foreground/[0.03] -mx-2.5 flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 transition-colors duration-300"
>
<span className="bg-foreground/[0.06] text-foreground/45 flex size-4 shrink-0 items-center justify-center rounded text-[9px] font-medium">
{result.domain.charAt(0).toUpperCase()}
</span>
<span className="text-foreground/90 min-w-0 flex-1 truncate text-[13.5px]">
{result.title}
</span>
<span className={cn(mono, "text-foreground/35 shrink-0")}>
{result.domain}
</span>
</div>
))}
</div>
</div>
);
}