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 linktarget="_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.

Ongoing charges must be disclosed to the client in cash terms[1] and rebalancing follows the house tolerance bands[2]. Hover either marker to peek its source.

<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>

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

  • Source rows 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 transition override, as on Reasoning); the markup never forks.

Props

ComponentPropTypeDefaultDescription
Sourcesopen / defaultOpen / onOpenChangeCollapsible propsclosedPlain disclosure state — no auto behaviours.
SourcesTriggercountnumberDrives the default label — "Used 1 source" / "Used N sources". Required.
SourcesTriggerchildrenReactNodelabel + caretReplace the default trigger content entirely.
SourcesContenttransitionTransitionhouse springOverride 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.
SourcesContentclassNamestringStyles the inner list box, not the animated wrapper.
SourcehrefstringThe source URL — rendered as a hardened external link. Required.
SourcetitlestringVisible citation title (content, not the HTML title attribute). Required.
SourcedescriptionstringOne-line context under the title — clamped to two lines.
SourceiconReactNodePhosphor GlobeLeading glyph, consumer-supplied — no favicon service is ever called.
Sourcetarget / relanchor props"_blank" / "noopener noreferrer"Hardened defaults; explicit props win.
SourcechildrenReactNodeicon + title + descriptionReplace the row content — the anchor and its hardening remain.
InlineCitationlabelstringThe marker index — rendered as [label]. Required.
InlineCitationtitlestringCitation title — shown in the hovercard and in the marker's accessible name. Required.
InlineCitationhrefstringThe source URL — the marker and the card's link both navigate to it (hardened). Required.
InlineCitationsnippetstringShort excerpt under the title — clamped to three lines.
InlineCitationchildrenReactNode[label]Override the marker content (stays inside the trigger anchor).
InlineCitationopenDelay / closeDelaynumber300 / 150Hover delays. The remaining root props configure the HoverCard; className styles the marker anchor.

On this page