The Citation component is an inline reference chip that displays source details in a rich hover preview card. It supports single-source previews, multi-source carousels, customizable triggers (favicon-only, text labels), and integration with LLM streaming outputs (e.g. converting [1] markdown markers to interactive components).
Preview
Usage
Installation
Manual Installation
Install @radix-ui/react-hover-card (which is wrapped by Citation):
pnpm add @radix-ui/react-hover-card
Copy the component code into your project from citation.tsx.
Basic Example
import {
Citation,
CitationContent,
CitationItem,
CitationTrigger,
} from "@/components/infinity-ui/citation";
export default function App() {
return (
<p>
React is a JavaScript library for building user interfaces
<Citation citations={[{ url: "https://react.dev", title: "React Dev" }]}>
<CitationTrigger className="ml-1" />
<CitationContent>
<CitationItem />
</CitationContent>
</Citation>
.
</p>
);
}
Examples
Multiple Sources (Carousel)
For citation groups representing multiple references, CitationCarousel wraps a smooth motion-driven slider inside the card popover.
Multiple Sources (Carousel Preview)
Trigger Customizations
Inline Copy Integration
Next.js is a powerful React framework for productionNext.js. It features hybrid static & server rendering, TypeScript support, smart bundling, and route pre-fetching without extra config. It is created and maintained by Vercel
Vercel.
import {
Citation,
CitationCarousel,
CitationCarouselContent,
CitationCarouselHeader,
CitationCarouselIndex,
CitationCarouselItem,
CitationCarouselNext,
CitationCarouselPagination,
CitationCarouselPrev,
CitationContent,
CitationItem,
CitationSourcesBadge,
CitationTrigger,
} from "@/components/infinity-ui/citation";
const sources = [
{ url: "https://wikipedia.org", title: "Wikipedia" },
{ url: "https://github.com", title: "GitHub" }
];
export default function App() {
return (
<Citation citations={sources}>
<CitationTrigger />
<CitationContent className="w-80">
<CitationCarousel>
<CitationCarouselHeader>
<CitationSourcesBadge />
<CitationCarouselPagination>
<CitationCarouselPrev />
<CitationCarouselIndex />
<CitationCarouselNext />
</CitationCarouselPagination>
</CitationCarouselHeader>
<CitationCarouselContent>
{sources.map((source, index) => (
<CitationCarouselItem key={source.url} index={index}>
<CitationItem />
</CitationCarouselItem>
))}
</CitationCarouselContent>
</CitationCarousel>
</CitationContent>
</Citation>
);
}
Vercel AI SDK Integration
When building chat interfaces with models that stream search resources (e.g. Perplexity Sonar), citations usually return as source-url message parts, accompanied by footnote numbers like [1] or [2] in the generated text.
You can intercept and render these markers as interactive hover cards by utilizing the following helpers:
1. Citation Parsing Utility
Create a utility script lib/inline-citations.tsx that replaces text footnotes with customized anchor links:
"use client";
import * as React from "react";
import {
Citation,
CitationCarousel,
CitationCarouselContent,
CitationCarouselHeader,
CitationCarouselIndex,
CitationCarouselItem,
CitationCarouselNext,
CitationCarouselPagination,
CitationCarouselPrev,
CitationContent,
CitationItem,
CitationSourcesBadge,
CitationTrigger,
type CitationSourceInput,
} from "@/components/infinity-ui/citation";
const GROUP_RE = /((?:\[\d+\])+)/g;
const ID_RE = /\[(\d+)\]/g;
const PREFIX = "https://citations.local/";
export function withInlineCitationLinks(text: string) {
return text.replace(GROUP_RE, (match) => {
const ids = [...match.matchAll(ID_RE)]
.map(([, id]) => Number(id))
.filter((id) => Number.isInteger(id) && id > 0);
if (ids.length === 0) return match;
const label = ids.map((id) => `[${id}]`).join("");
return `[${label}](${PREFIX}${ids.join(",")})`;
});
}
function parseIdsFromHref(href: string) {
if (!href.startsWith(PREFIX)) return [];
return href
.slice(PREFIX.length)
.split(",")
.map((id) => Number(id))
.filter((id) => Number.isInteger(id) && id > 0);
}
function citationsFromIds(ids: number[], sources: CitationSourceInput[]) {
return Array.from(new Set(ids))
.map((id) => sources[id - 1])
.filter(Boolean) as CitationSourceInput[];
}
export function createInlineCitationComponents(sources: CitationSourceInput[]) {
return {
a: ({ href, children, ...props }: any) => {
if (typeof href !== "string") {
return React.createElement("a", { ...props, href }, children);
}
const ids = parseIdsFromHref(href);
if (ids.length === 0) {
return React.createElement("a", { ...props, href }, children);
}
const citations = citationsFromIds(ids, sources);
if (citations.length === 0) return <>{children}</>;
return (
<>
{" "}
<Citation citations={citations}>
<CitationTrigger />
<CitationContent>
{citations.length > 1 ? (
<CitationCarousel>
<CitationCarouselHeader>
<CitationSourcesBadge />
<CitationCarouselPagination>
<CitationCarouselPrev />
<CitationCarouselIndex />
<CitationCarouselNext />
</CitationCarouselPagination>
</CitationCarouselHeader>
<CitationCarouselContent>
{citations.map((citation, index) => (
<CitationCarouselItem key={citation.url} index={index}>
<CitationItem />
</CitationCarouselItem>
))}
</CitationCarouselContent>
</CitationCarousel>
) : (
<CitationItem />
)}
</CitationContent>
</Citation>
</>
);
},
};
}
2. Stream sources in Next.js chat route
import { perplexity } from "@ai-sdk/perplexity";
import { convertToModelMessages, streamText, type UIMessage } from "ai";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: perplexity("sonar"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
sendSources: true,
});
}
API Reference
Citation
Root configuration component. Normalizes sources, manages slideshow state, and wraps the Radix Hover Card.
| Prop | Type | Default | Description |
|---|---|---|---|
citations | CitationSourceInput[] | [] | List of source URLs with optional titles and descriptions. |
open | boolean | — | Controlled open state of the hover card. |
defaultOpen | boolean | — | Uncontrolled initial open state. |
openDelay | number | 200 | Delay in ms before the card opens on hover. |
closeDelay | number | 200 | Delay in ms before the card closes on hover exit. |
CitationTrigger
The visual reference chip embedded inline.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Custom label override for the chip text (defaults to derived site name). |
showFavicon | boolean | true | Whether to render the source website's favicon icon. |
showSiteName | boolean | true | Whether to render the site name label next to the favicon. |
CitationContent
The popup container displaying detailed citation items.
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "center" | Alignment relative to the citation trigger pill. |
side | "top" | "bottom" | "left" | "right" | "bottom" | Placement of the popover relative to the trigger. |
sideOffset | number | 8 | Offset distance in pixels. |
CitationItem
Renders detailed information (title, description, and source link) inside an anchor tag wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
showTitle | boolean | true | Displays the document title. |
showDescription | boolean | true | Displays the snippet description. |
showSource | boolean | true | Displays the source footer containing site name and favicon. |
href | string | — | Custom link override (defaults to source url). |