Citation

Inline chip for showing a source reference with hover preview (favicon, label, and card preview).

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

WikipediaWorldometersMozillaGitHub

Usage

Installation

$ pnpm dlx shadcn add https://infinityui.vercel.app/r/citation.json

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

For citation groups representing multiple references, CitationCarousel wraps a smooth motion-driven slider inside the card popover.

Multiple Sources (Carousel Preview)

Wikipedia+2Hover to view multiple citations in a card carousel

Trigger Customizations

[1]Superscript marker
Favicon-only pill
WikipediaSite name only
Reference DocOverridden text label

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

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.

PropTypeDefaultDescription
citationsCitationSourceInput[][]List of source URLs with optional titles and descriptions.
openbooleanControlled open state of the hover card.
defaultOpenbooleanUncontrolled initial open state.
openDelaynumber200Delay in ms before the card opens on hover.
closeDelaynumber200Delay in ms before the card closes on hover exit.

CitationTrigger

The visual reference chip embedded inline.

PropTypeDefaultDescription
labelReactNodeCustom label override for the chip text (defaults to derived site name).
showFaviconbooleantrueWhether to render the source website's favicon icon.
showSiteNamebooleantrueWhether to render the site name label next to the favicon.

CitationContent

The popup container displaying detailed citation items.

PropTypeDefaultDescription
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.
sideOffsetnumber8Offset distance in pixels.

CitationItem

Renders detailed information (title, description, and source link) inside an anchor tag wrapper.

PropTypeDefaultDescription
showTitlebooleantrueDisplays the document title.
showDescriptionbooleantrueDisplays the snippet description.
showSourcebooleantrueDisplays the source footer containing site name and favicon.
hrefstringCustom link override (defaults to source url).