Display URL sources with favicon, title, and external link.
Getting Started
Add sources
npx shadcn@latest add @assistant-ui/sourcesThe @assistant-uinamespace resolves the Radix or Base UI flavor from your project's style through the style-aware registry entry in components.json. Without that entry, add by direct URL instead:
npx shadcn@latest add https://r.assistant-ui.com/base/sources.jsonMain Component
npm install @assistant-ui/react"use client";import { memo, useState, type ComponentProps } from "react";import { FileTextIcon } from "lucide-react";import type { VariantProps } from "class-variance-authority";import type { SourceMessagePartComponent } from "@assistant-ui/react";import { cn } from "@/lib/utils";import { Badge, badgeVariants } from "./badge";const extractDomain = (url: string): string => { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; }};const defaultFaviconUrl = (domain: string) => `https://icons.duckduckgo.com/ip3/${domain}.ico`;function SourceIcon({ url, className, faviconUrl = defaultFaviconUrl, ...props}: ComponentProps<"span"> & { url: string; faviconUrl?: (domain: string) => string;}) { const domain = extractDomain(url); const src = faviconUrl(domain); const [errorSrc, setErrorSrc] = useState<string | undefined>(undefined); const hasError = errorSrc === src; if (hasError) { return ( <span data-slot="source-icon-fallback" className={cn( "bg-muted flex size-3 shrink-0 items-center justify-center rounded-sm text-[10px] font-medium", className, )} {...props} > {domain.charAt(0).toUpperCase() || "?"} </span> ); } return ( <img data-slot="source-icon" src={src} alt="" className={cn("size-3 shrink-0 rounded-sm", className)} onError={() => setErrorSrc(src)} {...(props as ComponentProps<"img">)} /> );}function SourceTitle({ className, ...props }: ComponentProps<"span">) { return ( <span data-slot="source-title" className={cn("max-w-37.5 truncate", className)} {...props} /> );}function DocumentSourceIcon({ className, ...props }: ComponentProps<"span">) { return ( <span data-slot="source-document-icon" className={cn( "text-muted-foreground flex size-3 shrink-0 items-center justify-center", className, )} {...props} > <FileTextIcon className="size-3" /> </span> );}export type SourceProps = ComponentProps<"a"> & VariantProps<typeof badgeVariants>;function Source({ className, variant, size, target = "_blank", rel = "noopener noreferrer", ...props}: SourceProps) { return ( <a data-slot="source" className={cn( badgeVariants({ variant, size }), "focus-visible:border-ring focus-visible:ring-ring/50 cursor-pointer outline-none focus-visible:ring-[3px]", className, )} target={target} rel={rel} {...props} /> );}const SourcesImpl: SourceMessagePartComponent = (part) => { if (part.sourceType === "url" && part.url) { const domain = extractDomain(part.url); const displayTitle = part.title || domain; return ( <Source href={part.url}> <SourceIcon url={part.url} /> <SourceTitle>{displayTitle}</SourceTitle> </Source> ); } if (part.sourceType === "document") { return ( <Badge variant="secondary" className="focus-visible:border-ring focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]" > <span data-slot="source" className="inline-flex items-center gap-1.5"> <DocumentSourceIcon /> <SourceTitle>{part.title}</SourceTitle> </span> </Badge> ); } return null;};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, badgeVariants as sourceVariants,};assistant-ui dependencies
npm install @base-ui/react class-variance-authority"use client";import type { ComponentProps } from "react";import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";const badgeVariants = cva( "inline-flex items-center justify-center gap-1 rounded-md text-xs font-medium transition-colors [&_svg]:size-3 [&_svg]:shrink-0", { variants: { variant: { outline: "border-input text-muted-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground border bg-transparent", secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/80", muted: "bg-muted text-muted-foreground [a&]:hover:bg-muted/80 [a&]:hover:text-foreground", ghost: "text-muted-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground bg-transparent", info: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300 [a&]:hover:bg-blue-100/80", warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300 [a&]:hover:bg-amber-100/80", success: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300 [a&]:hover:bg-emerald-100/80", destructive: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300 [a&]:hover:bg-red-100/80", }, size: { sm: "px-1.5 py-0.5", default: "px-2 py-1", lg: "px-2.5 py-1.5 text-sm", }, }, defaultVariants: { variant: "outline", size: "default", }, },);export type BadgeProps = useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>;function Badge({ className, variant, size, render, ...props }: BadgeProps) { const dataProps = { "data-slot": "badge", "data-variant": variant, "data-size": size, } as ComponentProps<"span">; return useRender({ defaultTagName: "span", props: mergeProps<"span">( dataProps, { className: cn(badgeVariants({ variant, size }), className), }, props, ), render, });}export { Badge, badgeVariants };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>
{({ part }) => {
if (part.type === "source") return <Sources {...part} />;
return null;
}}
</MessagePrimitive.Parts>
</MessagePrimitive.Root>
);
};Variants
Use the variant prop to change the visual style. The default is outline.
<Source variant="outline" /> // Border (default)
<Source variant="ghost" /> // No background
<Source variant="muted" /> // Solid muted background
<Source variant="secondary" /> // Secondary background
<Source variant="info" /> // Blue
<Source variant="warning" /> // Amber
<Source variant="success" /> // Emerald
<Source variant="destructive" /> // RedSizes
Use the size prop to change the size.
<Source size="sm" /> // Small
<Source size="default" /> // Default
<Source size="lg" /> // LargeAPI Reference
Sources
The default export used as a SourceMessagePartComponent. Renders a single source part when sourceType === "url". Also exposes compound sub-components for custom layouts.
| Prop | Type | Default | Description |
|---|---|---|---|
url | string | — | The URL of the source (provided by the runtime) |
title | string | undefined | — | Display title; falls back to the domain if omitted |
sourceType | string | — | Must be "url" to render; other types are ignored |
Compound sub-components
import { Sources } from "@/components/assistant-ui/sources";
<Sources.Root href="https://example.com">
<Sources.Icon url="https://example.com" />
<Sources.Title>Example</Sources.Title>
</Sources.Root>| Sub-component | Equivalent named export | Description |
|---|---|---|
Sources.Root | Source | Root anchor element |
Sources.Icon | SourceIcon | Favicon with domain initial fallback |
Sources.Title | SourceTitle | Truncated title text |
Source
Root container rendered as an <a> tag. Accepts all <a> props plus variant and size.
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | URL the link points to |
variant | "outline" | "ghost" | "muted" | "secondary" | "info" | "warning" | "success" | "destructive" | "outline" | Visual style |
size | "sm" | "default" | "lg" | "default" | Size of the badge |
target | string | "_blank" | Link target |
rel | string | "noopener noreferrer" | Link rel attribute |
asChild | boolean | false | Merge props with a child element instead of rendering an anchor. Effective with the Radix badge; the Base UI badge ignores it |
className | string | — | Additional CSS classes |
SourceIcon
Displays the favicon for the given URL. Falls back to the domain initial inside a muted box when the favicon fails to load.
| Prop | Type | Default | Description |
|---|---|---|---|
url | string | — | URL used to derive the favicon and fallback initial |
faviconUrl | (domain: string) => string | DuckDuckGo's ip3 endpoint | Override the favicon source — useful in environments where the default service is unreachable |
className | string | — | Additional CSS classes applied to the <img> or fallback <span> |
Customizing the favicon provider
The default <Sources> component renders <SourceIcon> without forwarding faviconUrl, so the prop only applies when you compose Sources.Icon directly:
<Sources.Root href={url}>
<Sources.Icon
url={url}
faviconUrl={(domain) => `https://my-favicon-proxy.example.com/${domain}.ico`}
/>
<Sources.Title>{title}</Sources.Title>
</Sources.Root>To change the default for every source in your app, edit the defaultFaviconUrl constant at the top of your copied sources.tsx — that is the single source of truth used by <Sources>.
SourceTitle
Truncated title text rendered as a <span>.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Title content to display |
className | string | — | Additional CSS classes (default max-width is 37.5rem) |
sourceVariants
The underlying CVA variant function used to generate badge class names. Use this when building custom source-like components that need to match the built-in styling.
import { sourceVariants } from "@/components/assistant-ui/sources";
<span className={sourceVariants({ variant: "info", size: "sm" })}>
Custom badge
</span>Composable API
Use the named exports to build fully custom source layouts:
import { Source, SourceIcon, SourceTitle } from "@/components/assistant-ui/sources";
<Source href="https://example.com" variant="muted" className="gap-2">
<SourceIcon url="https://example.com" className="size-4" />
<SourceTitle className="max-w-none font-medium">Example</SourceTitle>
</Source>Related Components
- PartGrouping - Group sources by parentId