Citation chip
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.
npx shadcn@latest add hover-card badge
npm i lucide-react
Tokens this needs: --card, --card-foreground, --popover, --popover-foreground, --muted-foreground, --border, --status-warn, --chart-1
The status, chart, scale, state and direction names are an extension,
not a rename. shadcn has --destructive and five --chart-* and nothing
else in this territory.
────────────────────────────────────────────────────────────────────────
// CitationChip.tsx
────────────────────────────────────────────────────────────────────────
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 (
{source.title} {source.domain} · {source.date}
{source.passage ?? "No passage recorded. The link was matched to this sentence after the answer was written."}