Sources
Retrieval citations two ways — the Used-N-sources disclosure under a reply, and superscript inline markers whose hovercard peeks the source.
<Sources> <SourcesTrigger count={sources.length} /> <SourcesContent> {sources.map((source) => ( <Source key={source.url} href={source.url} title={source.title} description={source.description} /> ))} </SourcesContent></Sources>Usage
import { InlineCitation, Source, Sources, SourcesContent, SourcesTrigger } from "@reva/ui";Sources is the citations disclosure under a reply — "Used N sources" opening to a list of links. It is closed by default with plain open / defaultOpen / onOpenChange semantics — no auto behaviours — and expands on the same motion mechanism as Reasoning (the always-mounted, inert-while-closed panel; that page documents the mechanism in full). SourcesTrigger derives its label from count — "Used 1 source" singular, "Used N sources" otherwise — and children replace the label and caret entirely.
Each Source is a hardened external link — target="_blank" rel="noopener noreferrer" by default, explicit props win — with a leading glyph, a title, and an optional one-line description.
No favicon service — by design
The icon slot takes a consumer-supplied glyph and falls back to a Phosphor Globe. Unlike the AI Elements reference, no external favicon service is ever called — citation URLs never leak to a third party, and the list renders offline.
InlineCitation is the superscript marker for citations in running text — [1] after the claim it supports, peeking the source's title, snippet, and link in a hovercard on hover or keyboard focus. It is one lean export, no sub-parts: the marker is a hardened anchor and the HoverCard trigger in one, named Source {label}: {title} for screen readers, and sized in em so it tracks whatever text it sits in — body copy, list items, table cells. The hovercard layout is fixed: title, optional snippet, hostname link.
Composition
Use the following composition to build a Sources disclosure:
Sources (open / defaultOpen / onOpenChange — closed by default)
├── SourcesTrigger (count — "Used N sources" · rotating caret)
└── SourcesContent (always mounted, inert while closed)
└── Source (× n) (hardened external link — icon · title · description)InlineCitation composes nothing — it renders inline wherever the marker sits in the text.
Examples
Inline citations
The first marker below mounts open: hovercards can't be opened by synthetic events, so an instance that mounts open (defaultOpen — the root props configure the HoverCard) is the only honest static demo. Hover either marker for the real interaction.
<Text size="sm" leading="normal"> Ongoing charges must be disclosed to the client in cash terms <InlineCitation label="1" title={source.title} href={source.url} snippet={source.description} />{" "} and rebalancing follows the house tolerance bands <InlineCitation label="2" title={…} href={…} snippet={…} />.</Text>Upgrading Response links into citations
In a streamed reply the marker arrives as plain markdown — a link whose text is the bracketed index: [[1]](https://exact-source-url) parses to a link with text [1]. A components.a override on Response upgrades exactly those links into InlineCitations, keyed by exact URL match against the reply's sources, and falls back to the exported ResponseLink — Response's own default anchor renderer — for everything else:
import { InlineCitation, ResponseLink, type ResponseLinkProps } from "@reva/ui";
const CITATION_TEXT = /^\[(\d+)\]$/;
function createCitationComponents(sources: readonly SourceItem[]) {
const sourcesByUrl = new Map(sources.map((source) => [source.url, source]));
function CitationAwareLink({ node, href, children, ...props }: ResponseLinkProps) {
const label = typeof children === "string" ? CITATION_TEXT.exec(children)?.[1] : undefined;
const cited = label !== undefined && typeof href === "string" ? sourcesByUrl.get(href) : undefined;
if (label !== undefined && cited !== undefined) {
return (
<InlineCitation
label={label}
title={cited.title}
href={cited.url}
snippet={cited.description}
/>
);
}
// Everything else keeps the stock Response link behaviour — hardened
// external links, in-page anchors, inert still-streaming links.
return (
<ResponseLink node={node} href={href} {...props}>
{children}
</ResponseLink>
);
}
return { a: CitationAwareLink };
}
// Per message, memoized — Response compares `components` by identity.
const components = useMemo(() => createCitationComponents(message.sources), [message.sources]);
<Response components={components}>{message.text}</Response>;Two contracts from the Response page apply: the override map must be referentially stable (memoize it per message), and unmatched links must fall back to ResponseLink so they keep the stock behaviour. The exact-URL match also means a still-streaming link can never upgrade early — its href is still the incomplete-link sentinel until the URL completes. The marker matcher additionally handles link text arriving split across nodes.
Accessibility
Sourcerows and citation markers are real anchors, hardened by default — screen readers get ordinary links.- The hovercard is a sighted-pointer affordance: the marker's accessible name (
Source {label}: {title}) and the real link carry the semantics. Keyboard focus also opens the card, and on touch, tapping the marker simply navigates. - The disclosure panel is always mounted but inert while closed — out of the accessibility tree and tab order. Reduced motion collapses the expand spring to zero duration (enforced — it wins even over a consumer
transitionoverride, as on Reasoning); the markup never forks.
Props
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
Sources | open / defaultOpen / onOpenChange | Collapsible props | closed | Plain disclosure state — no auto behaviours. |
SourcesTrigger | count | number | — | Drives the default label — "Used 1 source" / "Used N sources". Required. |
SourcesTrigger | children | ReactNode | label + caret | Replace the default trigger content entirely. |
SourcesContent | transition | Transition | house spring | Override the expand/collapse spring. Under reduced motion the zero-duration transition is enforced — it beats this override. initial / animate / exit are owned by the component. |
SourcesContent | className | string | — | Styles the inner list box, not the animated wrapper. |
Source | href | string | — | The source URL — rendered as a hardened external link. Required. |
Source | title | string | — | Visible citation title (content, not the HTML title attribute). Required. |
Source | description | string | — | One-line context under the title — clamped to two lines. |
Source | icon | ReactNode | Phosphor Globe | Leading glyph, consumer-supplied — no favicon service is ever called. |
Source | target / rel | anchor props | "_blank" / "noopener noreferrer" | Hardened defaults; explicit props win. |
Source | children | ReactNode | icon + title + description | Replace the row content — the anchor and its hardening remain. |
InlineCitation | label | string | — | The marker index — rendered as [label]. Required. |
InlineCitation | title | string | — | Citation title — shown in the hovercard and in the marker's accessible name. Required. |
InlineCitation | href | string | — | The source URL — the marker and the card's link both navigate to it (hardened). Required. |
InlineCitation | snippet | string | — | Short excerpt under the title — clamped to three lines. |
InlineCitation | children | ReactNode | [label] | Override the marker content (stays inside the trigger anchor). |
InlineCitation | openDelay / closeDelay | number | 300 / 150 | Hover delays. The remaining root props configure the HoverCard; className styles the marker anchor. |
Actions
The quiet icon-action row under assistant replies — type-enforced accessible names, per-action tooltips, and an opt-in reveal-on-hover inside Message.
Conversation cards
The bespoke cards the assistant renders inline in a conversation, one per agent tool, presentational and mockable outside a live thread.