Elements

15 / 37 · 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.

Source

web-search.tsx
"use client";

import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, mono } from "./surfaces";

export interface WebSearchResult {
  title: string;
  domain: string;
}

export function WebSearch({
  query,
  results,
  visibleResults,
  searching,
  cycle,
  className,
}: {
  query: string;
  results: readonly WebSearchResult[];
  visibleResults: number;
  searching: boolean;
  cycle: number;
  className?: string;
}) {
  return (
    <div className={cn("flex w-full max-w-sm flex-col gap-2.5", className)}>
      <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 ? (
          <span className="relative inline-block leading-none">
            <span>Searching</span>
            <span
              aria-hidden
              className="shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
            >
              Searching
            </span>
          </span>
        ) : (
          <span className="fade-in animate-in duration-300">
            Read 3 sources
          </span>
        )}
      </div>
      <div className="flex min-h-[5.75rem] flex-col">
        {results.slice(0, 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>
  );
}