v1.5

Chat Source

Inline citation pills that credit an AI answer's sources, with a hover preview and a collapsible source list.

Pro

React ships a hook for optimistic updates, so a sent message can appear before the server confirms it useOptimisticuseOptimistic lets you show a different state while an async action is underway.. Keep the send control a real button so keyboard users can reach it Button patternA button is a widget that enables users to trigger an action or event..

Description

ChatSource is a small pill that cites where a statement came from. It renders an <a> when it has a link and a <span> otherwise, wrapped in an inline <span>, so it drops straight into a sentence inside a <p>. URL sources label themselves with the domain and a favicon (or the domain's initial), document sources get a file icon, and any source with a link can open a hover card with the title and a short summary. ChatSources is the companion disclosure that collapses every source behind one trigger under the answer.

Use it in assistant messages: inline after the sentence a source backs up, as numbered footnote markers, or as the full list of pages and files a retrieval step pulled in. href is filtered to http:, https:, mailto: and relative URLs, so model output can be passed in without a separate sanitizing step.

Don't use it for files the user attached to their own prompt. That is ChatAttachment. For an ordinary link in body copy use Text Link, and for a rich hover card that is not tied to a citation use Popover.

ChatSource is a Create UI Pro component. Install it with a Pro seat.

Installation

pnpm dlx @create-ui/cli add chat-source

Anatomy

<ChatSource>
  <ChatSource.Trigger>
    <ChatSource.Icon />
    <ChatSource.Title />
  </ChatSource.Trigger>
  <ChatSource.Preview />
</ChatSource>
 
<ChatSources>
  <ChatSources.Trigger />
  <ChatSources.Content>
    <ChatSources.List>
      <ChatSource />
    </ChatSources.List>
  </ChatSources.Content>
</ChatSources>

Usage

import { ChatSource, ChatSources } from "@/components/ui/chat-source"
<ChatSource href="https://react.dev" title="React" />

Examples

Every example is a Pro preview (marked with a Pro badge).

Favicon

faviconUrl puts the site's icon in the pill. Without one, or when the image fails to load, the pill shows the first letter of the label instead. Pass an element as a child of ChatSource.Icon for a custom glyph.

Pro

Long titles

The pill truncates at max-w-32 and the full title stays available in the hover preview. className on ChatSource.Trigger lands on the pill itself, so max-w-72 there gives it more room.

Pro
Central banks signal a pause in rate hikes as inflation cools across EuropePolicy makers point to slowing price growth ahead of the next meeting.Central banks signal a pause in rate hikes as inflation cools across EuropePolicy makers point to slowing price growth ahead of the next meeting.

Documents

sourceType="document" swaps the favicon for a file icon, and a source with no href is inferred as a document. Without a link the pill is static text. Give a document an href and it opens and previews like a web source.

Pro
Q3-launch-brief.pdfpricing-research.xlsxbrand-guidelines.pdfLogo clearspace, color tokens and type scale for launch assets.

Numbered citations

Pass the number as the child of ChatSource.Trigger for footnote-style markers, and render ChatSource.Preview yourself because children replace the default pair. A bare number is not a useful link name, so set aria-label.

Pro

Long tasks delay the next paint after a click, which is what INP measures 1INP measures how quickly a page responds to user input.. Breaking the work up and yielding between chunks lets the browser respond first 2Yields to the main thread so the browser can handle input..

Custom preview

Children of ChatSource.Preview replace the default card body and render inside PopoverBody, so they get the popover's padding. It forwards PopoverContent props, so size, side, align, sideOffset and showArrow work too. The preview only opens when the source has a link, since the link is its anchor.

Pro

Grouped sources

ChatSources collapses the full list under the answer. Stack a few ChatSource.Icons in the trigger; used outside a ChatSource, the icon reads faviconUrl from its own prop.

Pro

Controlled

Pass open and onOpenChange to drive the list from outside, for example from an "expand all" control. Use defaultOpen when you only need the starting state.

Pro

Accessibility

A linked pill is a regular <a> in the tab order. Focusing it opens the preview after openDelay, and moving focus away closes it. The card link repeats the pill's URL, so it is kept out of the tab order. ChatSources.Trigger is a <button> built on Radix Collapsible.

KeyDescription
TabMoves focus to the next pill or to the ChatSources trigger.
EnterOpens the focused source link in a new tab.
EscapeCloses the open hover preview.
Space / EnterToggles the ChatSources list when its trigger is focused.

ARIA notes:

  • The pill's accessible name is its visible text (the title, domain or file name). For a numbered citation, set aria-label on ChatSource.Trigger, e.g. Source 1: Interaction to Next Paint.
  • Favicons render with alt=""; the initial fallback, the document icon and the chevron are aria-hidden, so the label is announced once.
  • The preview is supplementary and does not move focus when it opens. Everything it shows should also be reachable through the link, because touch and screen reader users may never open it.
  • ChatSources.Trigger gets aria-expanded and aria-controls from Radix. Put a count or "sources" in its text so the button has a name.
  • The pill is 20px tall with its hit area stretched to 24px.
  • The preview animation, the collapse animation and the chevron rotation are disabled under prefers-reduced-motion.
  • See the Radix Popover and Collapsible docs for the underlying behavior.

Styling

Tailwind override: className on ChatSource.Trigger merges onto the pill. className on the ChatSource root only reaches the inline wrapper around it.

<ChatSource href="https://react.dev" title="React docs">
  <ChatSource.Trigger className="bg-medium max-w-60" />
</ChatSource>

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="chat-source" on the inline root wrapper, with data-source-type ("url" or "document").
  • data-slot="chat-source-trigger" on the pill (<a> with a link, <span> without). It carries data-state ("open" or "closed") while a preview is attached.
  • data-slot="chat-source-icon" on the favicon <img> or a custom icon.
  • data-slot="chat-source-icon-fallback" on the initial shown when there is no favicon or it fails to load.
  • data-slot="chat-source-document-icon" on the file icon.
  • data-slot="chat-source-title" on the truncating label.
  • data-slot="chat-source-preview" on the PopoverContent, with Radix data-state and data-side. The body inside keeps the popover's own data-slot="popover-body".
  • data-slot="chat-source-preview-link", -preview-header, -preview-title and -preview-description on the parts of the default card body.
  • data-slot="chat-sources" on the collapsible root and data-slot="chat-sources-trigger" on its button, both with Radix data-state.
  • data-slot="chat-sources-trigger-label" and data-slot="chat-sources-indicator" on the trigger's label row and chevron.
  • data-slot="chat-sources-content", data-slot="chat-sources-content-body" and data-slot="chat-sources-list" on the collapsible panel, its padded body and the wrapping row of pills.
[data-slot="chat-source"][data-source-type="document"]
  [data-slot="chat-source-trigger"] {
  /* ... */
}
  • Chat Attachment: use it for files the user attached to their own message.
  • Text Link: use it for an ordinary link in running text that is not a citation.
  • Popover: use it for click-triggered cards with interactive content.
  • File Format: use it when the file type needs a labeled badge rather than a citation.

API Reference

ChatSource

The root. Renders an inline <span> and extends React.ComponentProps<"span"> (minus title). With the default children it renders ChatSource.Trigger plus ChatSource.Preview, and wraps them in a Popover that opens on hover and focus when the preview is enabled.

Props

PropTypeDefaultDescription
hrefstring-Link target. Only http:, https:, mailto: and relative URLs render as a link.
titlestringdomain or file namePill label and preview title.
descriptionstring-Preview summary. Turns the preview on.
enablePreviewboolean!!(title || description)Forces the hover preview on or off. Never opens without a safe href.
faviconUrlstring-Favicon shown in the pill and the preview.
sourceType"url" | "document"href ? "url" : "document"Picks the favicon or the file icon.
openDelaynumber200Milliseconds before the preview opens.
closeDelaynumber200Milliseconds before the preview closes after the pointer or focus leaves.
classNamestring-Tailwind classes merged onto the inline wrapper via cn().
childrenReact.ReactNodetrigger + previewCustom composition. Replaces the default trigger and preview.

Variants

ChatSource has no variant axes. Restyle the pill through className on ChatSource.Trigger.

ChatSource.Trigger

The pill. Renders an <a> (new tab, rel="noopener noreferrer") when the source has a safe href, otherwise a <span>. Extends React.HTMLAttributes<HTMLElement> plus the anchor attributes below, and becomes the PopoverAnchor when the preview is enabled, so a click still follows the link.

Props

PropTypeDefaultDescription
labelReact.ReactNode-Shorthand pill body, used when there are no children.
targetstring"_blank"Anchor target. Ignored without a link.
relstring"noopener noreferrer"Anchor rel. Ignored without a link.
referrerPolicyReact.HTMLAttributeReferrerPolicy-Anchor referrer policy. Ignored without a link.
downloadboolean | string-Anchor download hint. Ignored without a link.
classNamestring-Tailwind classes merged onto the pill via cn().
childrenReact.ReactNodeicon + titlePill content.

ChatSource.Icon

The favicon. Renders an <img> (loading="lazy", referrerPolicy="no-referrer"), the label's initial when there is no favicon or it fails, or the element you pass. Extends React.HTMLAttributes<HTMLElement>. Works outside a ChatSource when given faviconUrl.

Props

PropTypeDefaultDescription
faviconUrlstringroot faviconUrlImage URL. Overrides the root value.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Custom icon. An element receives the icon classes; text is wrapped in a span.

ChatSource.DocumentIcon

The file icon used by document sources. Extends the props of RiFileTextLine from @create-ui/assets/icons and is always aria-hidden.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().

ChatSource.Title

The truncating label. Extends React.ComponentProps<"span">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNoderoot labelText shown in the pill.

ChatSource.Preview

The preview card. Renders PopoverContent (size="md", side="top" by default) with its content inside PopoverBody, and extends its props (minus title). Renders nothing unless the source has a safe href and the preview is enabled.

Props

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Popover size: width, radius and body spacing.
titlestringroot titleTitle in the default card body.
descriptionstringroot descriptionSummary in the default card body.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNodedomain, title, summaryCustom card body.

ChatSources

The disclosure for a list of sources. Wraps Radix Collapsible.Root and extends its props.

Props

PropTypeDefaultDescription
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Fires when the open state changes.
disabledbooleanfalseStops the trigger from toggling.
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-ChatSources.Trigger and ChatSources.Content.

ChatSources.Trigger

The toggle button, with a chevron that turns when open. Wraps Radix Collapsible.Trigger and extends its props.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Label, optionally with stacked ChatSource.Icons.

ChatSources.Content

The animated panel. Wraps Radix Collapsible.Content and extends its props.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-Usually a ChatSources.List.

ChatSources.List

A wrapping row of pills. Extends React.ComponentProps<"div">.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged via cn().
childrenReact.ReactNode-ChatSource elements.

Variant helpers

Every part's classes are exported as a CVA function with no variant axes, for building look-alike elements: chatSourceVariants, chatSourceTriggerVariants, chatSourceIconVariants, chatSourceIconFallbackVariants, chatSourceTitleVariants, chatSourcesVariants, chatSourcesTriggerVariants, chatSourcesContentVariants and chatSourcesListVariants.