Skip to content
KONIGI Patterns

AI Assistants / Understanding answers / Citation chip

1 of 5 ›

Citation chip

Readers need to check the source behind a specific claim.

Updated September 12, 2026

Problem

An AI answer can mix sourced facts with unsupported statements. Without a source beside each claim, readers cannot tell which statements they can check.

Solution

Place a source marker beside the claim it supports. Let people open it to see the source title, publisher or domain, date, and relevant passage when available.

Make markers usable with a keyboard and on touch screens. Keep them small enough to read past, and group multiple sources when they would crowd a sentence.

Check that each linked source supports the claim. Finding a relevant page does not establish that every statement in the answer came from it. Mark known access restrictions before people open a link.

Use when

Answers make factual claims based on retrieved documents or web pages.

Don’t use when

The output is the model’s own composition: a draft, a summary of text the viewer supplied, a rewrite, a piece of code. Citing sources for a generated first draft implies an authority the output doesn’t have.

Trade-offs

Frequent citations can interrupt reading. They can also make an answer seem verified even when its sources do not support it. A source behind a login or paywall may be useful, but some readers will be unable to check it.

Checklist

  • Is the marker attached to a claim or to the whole response?
  • How often does the cited page actually support the sentence, and is that measured?
  • Does the chip expose the source’s date?
  • What does a reader see on hover, on focus, and on a touch device?
  • What happens when three sentences in a row share one source?
  • Is a paywalled or permission-restricted source marked before the reader clicks?
  • Do chips survive copying the answer into another document?
  • Can a reader tell which parts of an answer have no source at all?
  • Does a dead link fail visibly or silently?
  • Are chips reachable and announced in a sensible order by a screen reader?

Compare

Perplexity puts numbered chips inline and resolves the source set before the prose begins, so the citation a sentence points at already exists when the sentence arrives. ChatGPT surfaces sources when a search tool ran and leaves unsourced prose unmarked. A chip means something there, instead of being ambient decoration. Gemini offers a separate verification pass against search rather than treating inline attribution as complete, which concedes that the chip and the check are different operations. Microsoft Copilot inside a tenant cites internal documents, where the hard part is that a cited file may be one the reader has no permission to open.

Source list collects the sources for the whole answer. Tool call trace shows the searches or lookups that found them.

Reasoning disclosure shows a summary of the AI’s approach. Knowledge cutoff notice flags answers that may be outdated. Refusal applies when the assistant cannot provide a supported answer.

Citation chip anatomy A paragraph in which two sentences carry source chips and one carries none, with an expanded card showing the domain, the date and the passage, and an unmarked sentence flagged as the one nothing supports. Attached to the claim, not to the answer w3.org mdn.org no source Using server-sent events developer.mozilla.org · March 2026 the chip earns trust whether or not the page supports the sentence 1 3 2 4 1 ONE CLAIM, ONE SOURCE A list under the answer leaves the reader to guess which sentence came from where. 2 DOMAIN AND DATE Domain is the fastest trust signal. Date is the most omitted and often decisive. 3 THE UNMARKED SENTENCE Chips only mean something if a reader can see which parts have nothing behind them. 4 TRUST WITHOUT CHECKING Readers see citations and stop verifying, so a decorative chip is worse than none. Measure how often the cited page actually supports the sentence. That number is the pattern.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

A source attached to the claim rather than to the whole answer. The chip earns trust whether or not the page behind it supports the sentence, which is why the expanded card has to show the passage and the date.

shadcn
npx shadcn@latest add hover-card badge
npm
lucide-react
Tokens
--card--card-foreground--popover--popover-foreground--muted-foreground--border--status-warn--chart-1
Server-sent events keep one HTTP response open and push text down it as the build writes.
w3.org
Each message is a block of data: lines ended by a blank line, and the browser's EventSource object hands it to a listener and reconnects on its own if the connection drops.
mdn.org

Using server-sent events

developer.mozilla.org · March 2026

The event stream is a simple stream of text data which must be encoded using UTF-8. Messages in the event stream are separated by a pair of newline characters.

For a build log this is cheaper than polling and simpler than a WebSocket.no source

CitationChip.tsxA chip on the clause it belongs to, a card with the domain, the date and the passage, and a mark on the sentence that has none.

import { Lock } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";

/**
 * A source is four things, and the date is one of them. A chip that cannot
 * say when its page was written is pointing at something the reader cannot
 * weigh, so `date` is required rather than an optional line in the card.
 */
export type Source = {
  title: string;
  /** What the chip shows. Short, because it sits inside a sentence. */
  site: string;
  /** The full host, shown in the card. Domain is the fastest trust signal. */
  domain: string;
  /** As the source states it: "March 2026", "12 May 2024". */
  date: string;
  url: string;
  /** The passage the claim was drawn from. Missing means the attribution was reconstructed, and the card says so. */
  passage?: string;
  /** Paywalled or permission-restricted. Marked before the reader clicks, since a chip they cannot open is worse than none. */
  gated?: boolean;
};

/** One sentence and what stands behind it. `null` is a real value: it means the
 *  sentence is the model's own, and the paragraph shows that rather than hiding it. */
export type Claim = { text: string; source: Source | null };

export function CitationChip({ source, defaultOpen = false, onOpen }: {
  source: Source;
  /** Start with the card showing, as in a demo. */
  defaultOpen?: boolean;
  /** Fires when the card opens. Count these against clicks through: it is the number this pattern is measured by. */
  onOpen?: (source: Source) => void;
}) {
  return (
    <HoverCard defaultOpen={defaultOpen} openDelay={150} closeDelay={200} onOpenChange={(o) => o && onOpen?.(source)}>
      <HoverCardTrigger asChild>
        <a href={source.url} target="_blank" rel="noreferrer" className="ml-1.5 inline-flex align-baseline no-underline">
          <Badge variant="outline" className="h-4 gap-1 rounded-full px-1.5 py-0 text-[10px] font-normal text-chart-1 border-chart-1">
            {source.gated && <Lock className="size-2.5" aria-label="sign-in required" />}
            {source.site}
          </Badge>
        </a>
      </HoverCardTrigger>
      <HoverCardContent side="bottom" align="end" className="w-72 p-3">
        <p className="text-sm text-card-foreground">{source.title}</p>
        <p className="mt-1 text-[11px] text-muted-foreground">{source.domain} · {source.date}</p>
        <p className="mt-2.5 border-t pt-2.5 text-xs leading-relaxed text-muted-foreground">
          {source.passage ?? "No passage recorded. The link was matched to this sentence after the answer was written."}
        </p>
      </HoverCardContent>
    </HoverCard>
  );
}

/**
 * The paragraph, with the chip on the clause it belongs to. A sentence with
 * nothing behind it is marked, because chips only mean something when the
 * reader can see which parts have none.
 */
export function CitedParagraph({ claims, openSource, onOpen }: {
  claims: Claim[];
  /** Which source's card starts open, by url. */
  openSource?: string;
  onOpen?: (source: Source) => void;
}) {
  // A div, not a p: the chip is a hover card whose content holds paragraphs,
  // and a p cannot contain them.
  return (
    <div className="text-sm leading-7 text-card-foreground">
      {claims.map((c, i) => (
        <span key={i} className={c.source ? undefined : "decoration-status-warn/60 underline decoration-dotted underline-offset-4"}>
          {c.text}
          {c.source
            ? <CitationChip source={c.source} defaultOpen={c.source.url === openSource} onOpen={onOpen} />
            : <span className="ml-1.5 text-[10px] text-status-warn">no source</span>}
          {i < claims.length - 1 && " "}
        </span>
      ))}
    </div>
  );
}

demo.tsxHow it is called: three claims, two sourced, the MDN card open.

import { CitedParagraph, type Source } from "./CitationChip";

/**
 * Three sentences about the slow build's log stream. Two are drawn from pages
 * the assistant retrieved and carry a chip; the third is the model's own
 * inference and is marked as such. The MDN card starts open.
 */
const W3C: Source = {
  title: "Server-Sent Events, W3C Recommendation",
  site: "w3.org",
  domain: "www.w3.org",
  date: "February 2015",
  url: "https://www.w3.org/TR/eventsource/",
  passage: "The EventSource interface is used to receive server-sent events. It connects to a server over HTTP and receives events in text/event-stream format without closing the connection.",
};

const MDN: Source = {
  title: "Using server-sent events",
  site: "mdn.org",
  domain: "developer.mozilla.org",
  date: "March 2026",
  url: "https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events",
  passage: "The event stream is a simple stream of text data which must be encoded using UTF-8. Messages in the event stream are separated by a pair of newline characters.",
};

export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4 pb-32">
      <CitedParagraph
        openSource={MDN.url}
        onOpen={(s) => console.log("opened", s.domain)}
        claims={[
          { text: "Server-sent events keep one HTTP response open and push text down it as the build writes.", source: W3C },
          { text: "Each message is a block of data: lines ended by a blank line, and the browser's EventSource object hands it to a listener and reconnects on its own if the connection drops.", source: MDN },
          { text: "For a build log this is cheaper than polling and simpler than a WebSocket.", source: null },
        ]}
      />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No screenshots yet for this pattern. Browse Products to see other examples.