Display URL sources with favicon, title, and external link.
Getting Started
Add sources
npx shadcn@latest add @assistant-ui/sourcesMain Component
npm install @assistant-ui/react @radix-ui/react-slot class-variance-authority"use client";import { memo, useState } from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import type { SourceMessagePartComponent } from "@assistant-ui/react";import { cn } from "@/lib/utils";const extractDomain = (url: string): string => { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; }};const getDomainInitial = (url: string): string => { const domain = extractDomain(url); return domain.charAt(0).toUpperCase();};const sourceVariants = cva( "inline-flex cursor-pointer items-center gap-1.5 rounded-md text-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", { variants: { variant: { default: "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", outline: "border bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground", ghost: "text-muted-foreground hover:bg-accent hover:text-accent-foreground", }, size: { default: "px-2 py-1", sm: "px-1.5 py-0.5", lg: "px-2.5 py-1.5", }, }, defaultVariants: { variant: "default", size: "default", }, },);function SourceIcon({ url, className, ...props}: React.ComponentProps<"span"> & { url: string }) { const [hasError, setHasError] = useState(false); const domain = extractDomain(url); if (hasError) { return ( <span data-slot="source-icon-fallback" className={cn( "flex size-3.5 shrink-0 items-center justify-center rounded-sm bg-muted font-medium text-[10px]", className, )} {...props} > {getDomainInitial(url)} </span> ); } return ( <img data-slot="source-icon" src={`https://www.google.com/s2/favicons?domain=${domain}&sz=32`} alt="" className={cn("size-3.5 shrink-0 rounded-sm", className)} onError={() => setHasError(true)} {...(props as React.ComponentProps<"img">)} /> );}function SourceTitle({ className, ...props }: React.ComponentProps<"span">) { return ( <span data-slot="source-title" className={cn("max-w-37.5 truncate", className)} {...props} /> );}export type SourceProps = React.ComponentProps<"a"> & VariantProps<typeof sourceVariants> & { asChild?: boolean; };function Source({ className, variant, size, asChild = false, ...props}: SourceProps) { const Comp = asChild ? Slot : "a"; return ( <Comp data-slot="source" data-variant={variant} data-size={size} className={cn(sourceVariants({ variant, size, className }))} {...props} /> );}const SourcesImpl: SourceMessagePartComponent = ({ url, title, sourceType,}) => { if (sourceType !== "url" || !url) return null; const domain = extractDomain(url); const displayTitle = title || domain; return ( <Source href={url} target="_blank" rel="noopener noreferrer"> <SourceIcon url={url} /> <SourceTitle>{displayTitle}</SourceTitle> </Source> );};const Sources = memo(SourcesImpl) as unknown as SourceMessagePartComponent & { Root: typeof Source; Icon: typeof SourceIcon; Title: typeof SourceTitle;};Sources.displayName = "Sources";Sources.Root = Source;Sources.Icon = SourceIcon;Sources.Title = SourceTitle;export { Sources, Source, SourceIcon, SourceTitle, sourceVariants };Use in your application
Pass Sources to MessagePrimitive.Parts:
import { Sources } from "@/components/assistant-ui/sources";
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root className="...">
<MessagePrimitive.Parts
components={{
Source: Sources,
}}
/>
</MessagePrimitive.Root>
);
};Variants
Use the variant prop to change the visual style.
<Source variant="default" /> // Subtle background
<Source variant="secondary" /> // Secondary color
<Source variant="outline" /> // Border only
<Source variant="ghost" /> // No backgroundSizes
Use the size prop to change the size.
<Source size="sm" /> // Small
<Source size="default" /> // Default
<Source size="lg" /> // LargeComposable API
The component exports composable sub-components:
import { Source, SourceIcon, SourceTitle } from "@/components/assistant-ui/sources";
<Source href="https://example.com" className="gap-2">
<SourceIcon url="https://example.com" className="size-4" />
<SourceTitle className="max-w-none font-medium">Example</SourceTitle>
</Source>| Component | Description |
|---|---|
Source | Root container, renders as <a> |
SourceIcon | Favicon with domain initial fallback |
SourceTitle | Truncated title text |
Related
- PartGrouping - Group sources by parentId