DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. Citations

Citations

Inline citation markers for a streamed AI answer and the numbered source list they point at — pending until their source arrives, named links with a preview on hover and focus, and a highlight shared between marker and entry.

$ pnpm add @tessinaui/ui

When to use

  • An AI answer that cites its sources. Each claim carries a numbered marker in the text and the sources are listed under the answer.
  • A streamed answer. A marker can arrive before its source does; it shows as pending and becomes the numbered link in place, so the text never reflows or renumbers.
  • A quick check without leaving the page. Hover or focus a marker and a short preview names the source; the list entry highlights with it.
  • Sources the reader should be able to verify. Give each one a title and a url; add a domain, description or icon when they help the reader recognise it.
  • A list of your own. useCitations() returns the numbered entries when the stock list does not fit.

When not to use

  • A link in prose. That is an ordinary markdown link, and citationComponents hands it back to the answer unchanged. Use Link outside an answer.
  • Sources with no inline markers. Use ChatBubbleSources on its own; see ChatBubble.
  • Footnotes. A [^1] needs a definition that arrives in a later block, so it cannot be matched while the answer streams. Cite with #cite:<id> instead.
  • Answers that do not stream or use markdown. Markers are drawn from the answer's markdown; render the text with StreamingResponse so they resolve.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Citations, citationComponents } from "@tessinaui/ui/citations";
import { StreamingResponse } from "@tessinaui/ui/streaming-response";
import { ChatBubbleSource, ChatBubbleSources } from "@tessinaui/ui/chat-bubble";

<Citations sources={sources}>
  <StreamingResponse markdown={text} components={citationComponents} />
  <ChatBubbleSources label="Sources">
    {sources.map((s) => (
      <ChatBubbleSource key={s.id} citationId={s.id} />
    ))}
  </ChatBubbleSources>
</Citations>

In the answer, a citation is an inline markdown link whose href names a source: [1](#cite:shade). The label is ignored — the marker draws its own number — and the #cite: href is discarded: the marker links to the source itself.

Examples

Default

Hover or focus a marker and its entry in the list highlights, and the other way round.

Streaming

Sources arrive with the text, and not always before the citation that points at them. A marker whose source has not arrived is a pending chip with a dot; when the source lands, the same element becomes the numbered link. A citation that is still being typed is a pending marker too, from the # after ]( onward — never the raw [1](#cite:sh text. The [1]( before that # could be any link, so for a token it shows as text, as any half-typed link does.

Source list

CitationSources is the answer's sources as one row under it: a pill with the stacked marks and the count, which opens to a card per source. Each card shows the source's mark and site, its number at the end, and its title beneath. Inside Citations a card takes its source's number and link and highlights with the answer's markers. The cards are two columns in a wide row and one snap-scrolling row in a narrow one. It renders nothing without sources. CitationMarkStack is the overlapping marks on their own, for a count line or a header; it is decorative, so pair it with text that says what it counts.

In a conversation

On color

Right to left

API Reference

Citations

PropTypeDefaultDescription
sourcesCitationSource[][]The answer's sources, in numbering order. Append only
dir"ltr" | "rtl"—"rtl" picks the Arabic strings; layout follows the page
stringsPartial<CitationsStrings>—Per-entry string overrides

CitationSource

FieldTypeDescription
idstringWhat #cite:<id> in the answer points at
titlestringShown in the preview and the list
urlstringWhere the marker and the list entry link, in a new tab
domainstringShown in the marker's name and the preview; read from url when omitted
descriptionstringA line of excerpt in the preview
iconReactNodeA favicon or logo. The library fetches nothing — pass your own

CitationMarker

Rendered for you by citationComponents; use it directly in content you render yourself.

PropTypeDefaultDescription
sourceIdstring—The source this marker cites
size"xs" … "xl"the answer'sType size of the number
tone"default" | "on-color"the answer'sInherited from StreamingResponse, then the surface

CitationSources

PropTypeDefaultDescription
sourcesCitationSource[]—The sources to list. Renders nothing when empty
open · defaultOpen · onOpenChangebooleanclosedWhether the cards show
dir"ltr" | "rtl"—"rtl" picks the Arabic strings
stringsPartial<CitationSourcesStrings>—count and countOne (with {count}), and list, the name of the card list

CitationMarkStack

PropTypeDefaultDescription
sourcesCitationSource[]—The sources whose marks stack
maxnumber4How many marks to stack

The marks part along the row on hover, focus and while open, so a count beside them never sits under a mark. Under reduced motion they part at once.

citationComponents

{ a: CitationAnchor }, one frozen object. Pass it as StreamingResponse's components. To add your own overrides, spread it into a map you also keep stable — { ...citationComponents, p: MyParagraph } defined outside the render — because a new map on every render re-renders every block of the answer.

ChatBubbleSource citationId

Tied to a source, the chip takes that source's number, link, title and icon (anything you pass wins), opens it in a new tab, and highlights with the answer's markers for that source. Without citationId it is unchanged.

DOM contract

AttributeWhereValues
data-slotthe marker, the previewcitation-marker, citation-preview, citation-sources, citation-mark-stack
data-statethe markerpending · resolved
data-highlightedthe marker, a tied chippresent while its source is hovered or focused anywhere

Numbering

A marker's number is its source's position in sources, after duplicate ids are dropped (the first occurrence wins). Nothing is counted from the text, so numbers cannot reshuffle as more of the answer arrives — as long as sources only ever grows by appending, which is how a streamed array arrives anyway. Reordering or removing an entry renumbers every marker after it, and warns once in development.

useCitations() returns the numbered, deduplicated entries, for a source list of your own:

example.tsx
function SourceList() {
  const entries = useCitations()?.entries ?? [];
  return entries.map((e) => <ChatBubbleSource key={e.source.id} citationId={e.source.id} />);
}

The wire format

  • Always the fragment form, #cite:<id>. A bare cite: scheme is not a safe protocol, and react-markdown rewrites it to an empty href — a link to the current page.
  • Ids are A–Z a–z 0–9 _ . -. Other characters reach the marker percent-encoded; they are decoded, but keep ids plain.
  • Footnotes do not work. A [^1] needs its [^1]: … definition, which arrives in a later block and is parsed on its own.
  • Without citationComponents, a citation renders as an ordinary in-page link, so an answer stays readable in any markdown renderer.

Accessibility

  • A marker is a link with a real name: "Source 3, example.com (opens in a new tab)", never a bare number. The visible number is part of the name.
  • A pending marker is not a link. Without an href an <a> is a generic element — no role, no tab stop, and no name (one is not allowed there). It becomes the link in place, the same element, when its source arrives, and while the answer streams nothing inside it is spoken (StreamingResponse keeps its content out of live regions). A citation whose source never arrives stays a plain chip that assistive technology skips.
  • The preview opens on keyboard focus as well as hover, and Escape closes it without moving focus. Its title, domain and link are also in the marker's name and the source list; a source's description appears only in the preview, so keep anything a reader must know out of it.
  • Touch: a tap opens the preview rather than following the link, so a mis-tap does not navigate away from an answer; the preview's title is the link. Every marker's touch target is 44px tall without changing the line height, and exactly as wide as the marker: a wider one would reach past the answer at the end of a line and scroll the page sideways. Across the line, a marker is an inline target in a sentence, which WCAG 2.5.8 exempts.
  • Not a superscript. The number sits on the baseline at the meta size, 12px and up; a superscript of body text would be under the type floor.
  • On a coloured plate the marker is the plate's soft chip, its highlight the plate's hairline, and its focus ring the plate's ink.

AT matrix: NOT yet screen-reader tested (hence beta). NVDA and JAWS: untested, no Windows host (QA §5.6).

StreamingResponseToolCall

On this page

When to useWhen not to useInstallationUsageExamplesDefaultStreamingSource listIn a conversationOn colorRight to leftAPI ReferenceCitationsCitationSourceCitationMarkerCitationSourcesCitationMarkStackcitationComponentsChatBubbleSource citationIdDOM contractNumberingThe wire formatAccessibility