# Citation



import CitationDefault from "@/components/nexus-ui/examples/citation/default";
import CitationMultipleSources from "@/components/nexus-ui/examples/citation/multiple-sources";
import CitationTriggerVariants from "@/components/nexus-ui/examples/citation/trigger-variants";
import CitationInlineWithText from "@/components/nexus-ui/examples/citation/inline-with-text";
import { Callout } from "@/components/callout";

Inline chip for showing a **source reference** with **hover preview** (favicon, label, and card copy for title, description, and link). Pass **`citations`** as an array of **`{ url, title?, description? }`**—**one entry** gives a single citation; **several entries** pair with **`CitationCarousel`** and **`CitationCarouselItem`** so users can move between sources in the same preview.

<DemoWithCode src="components/nexus-ui/examples/citation/default.tsx">
  <CitationDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add carousel hover-card && npm install radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add carousel hover-card && pnpm add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add carousel hover-card && yarn add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add carousel hover-card && bun add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/citation.tsx" title="components/nexus-ui/citation.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noCollapse
import {
  Citation,
  CitationContent,
  CitationItem,
  CitationTrigger,
} from "@/components/nexus-ui/citation";
```

```tsx keepBackground noCollapse
<Citation citations={[{ url: "https://example.com", title: "Example", description: "…" }]}>
  <CitationTrigger />
  <CitationContent>
    <CitationItem />
  </CitationContent>
</Citation>
```

`CitationItem` renders a default **`h4`** title, **`p`** description, and **`CitationSource`** footer. Toggle blocks with **`showTitle`**, **`showDescription`**, and **`showSource`**, or pass **`children`** to replace the default layout entirely (e.g. custom order or **`CitationSource`** only).

Examples [#examples]

Multiple sources [#multiple-sources]

With more than one **`citations`** entry, the default chip shows **`+N`** after the first source. **`CitationCarousel`** wraps a shadcn **`Carousel`** component inside the hover card so each source is one slide—users move between them with prev/next (or swipe) without closing the preview.

<DemoWithCode src="components/nexus-ui/examples/citation/multiple-sources.tsx">
  <CitationMultipleSources />
</DemoWithCode>

Trigger label and favicon [#trigger-label-and-favicon]

The built-in **`CitationTrigger`** chip can be adjusted with **`showFavicon`** and **`showSiteName`**. Use **`label`** to replace the auto-derived site name with any string—numbers, hostname, or text from **`resolveCitationSource`** / **`parseCitationUrl`** when you want it derived in code.

<DemoWithCode src="components/nexus-ui/examples/citation/trigger-variants.tsx">
  <CitationTriggerVariants />
</DemoWithCode>

Inline with text [#inline-with-text]

The trigger can sit **inline** with surrounding copy—place **`Citation`** where a phrase or sentence needs a source chip.

<DemoWithCode src="components/nexus-ui/examples/citation/inline-with-text.tsx">
  <CitationInlineWithText />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use **Citation** with [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) when your model returns **sources** as [`UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message) **`parts`** (for example **`type: "source-url"`**). The AI SDK does not standardize citation markup across providers, and [Streamdown](https://streamdown.ai/) does not resolve footnotes for you—so you collect URLs from message parts, match them to inline markers like **`[1]`** in the assistant text, and swap those markers for **`Citation`** inside markdown by overriding the anchor renderer.

See [Prompt Input](/docs/components/prompt-input#vercel-ai-sdk-integration) for the same **`POST /api/chat`** shape; below extends it with **`sendSources: true`** and a client-side inline-citation pipeline.

<Callout type="info">
  Citation UI depends on providers and models that emit source parts. If your model returns text only, there are no citations to render.
</Callout>

<Steps>
  <Step>
    <h3>
      Install the AI SDK and a provider that emits sources
    </h3>

    Add **`ai`**, **`@ai-sdk/react`**, and a provider package whose models surface search or citation URLs in the UI message stream (for example **`@ai-sdk/perplexity`** for Sonar). Many chat models stream text only—without **`source-url`** (or equivalent) parts, you have nothing to map **`[1]`** to.

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/perplexity
    ```
  </Step>

  <Step>
    <h3>
      Create a chat API route that streams sources
    </h3>

    Call **`streamText`**, then return **`toUIMessageStreamResponse({ sendSources: true })`** so the client receives source metadata alongside text. Without **`sendSources`**, assistant messages may omit **`source-url`** parts even when the underlying model found URLs. Pick a model your provider documents as search- or citation-capable.

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, type UIMessage } from "ai";
    import { perplexity } from "@ai-sdk/perplexity";

    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,
      });
    }
    ```
  </Step>

  <Step>
    <h3>
      Add helpers that turn markers into citation UI
    </h3>

    Assistant prose often contains bracket references (**`[1]`**, **`[1][2][4]`**). Treat the ordered list of **`source-url`** parts as the bibliography: **`[1]`** is the first URL, **`[2]`** the second, and so on. **`withInlineCitationLinks`** rewrites each run of adjacent markers into a single markdown link whose **`href`** uses a dedicated **`https://…`** prefix (custom schemes are often stripped by sanitizers). **`createInlineCitationComponents`** passes a custom **`a`** component to Streamdown: real links stay as **`<a>`**; marker links parse the id list, resolve rows from your sources array, and render **`Citation`**, **`CitationTrigger`**, and **`CitationContent`**.

    ```tsx title="lib/inline-citations.tsx"
    "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/nexus-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>
            </>
          );
        },
      };
    }
    ```
  </Step>

  <Step>
    <h3>
      Render assistant markdown with 

      `useChat`

       and MessageMarkdown
    </h3>

    For each assistant message, join **`text`** parts (for example with **`isTextUIPart`**), collect **`source-url`** parts into **`{ url, title }`** objects for **`Citation`**, then pass **`createInlineCitationComponents(sources)`** into **`MessageMarkdown`** via **`components`**. Feed **`withInlineCitationLinks(text)`** as children so markers become links before Streamdown renders. The example below shows one assistant turn; in a full chat, map **`messages`** the same way as in [Message](/docs/components/message#vercel-ai-sdk-integration).

    ```tsx title="app/citations-chat/page.tsx"
    "use client";

    import * as React from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
    import { MessageMarkdown } from "@/components/nexus-ui/message";
    import {
      createInlineCitationComponents,
      withInlineCitationLinks,
    } from "@/lib/inline-citations";

    function textFromMessage(message: UIMessage) {
      return message.parts.filter(isTextUIPart).map((p) => p.text).join("");
    }

    function sourceUrlPartsFromMessage(message: UIMessage) {
      return message.parts.filter(
        (p): p is Extract<UIMessage["parts"][number], { type: "source-url" }> =>
          p.type === "source-url",
      );
    }

    export default function CitationsChatPage() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      const text = textFromMessage(assistant);
      const sources = sourceUrlPartsFromMessage(assistant).map((s) => ({
        url: s.url,
        title: s.title?.trim() || s.url,
      }));

      return (
        <MessageMarkdown components={createInlineCitationComponents(sources)}>
          {withInlineCitationLinks(text)}
        </MessageMarkdown>
      );
    }
    ```
  </Step>
</Steps>

Provider limits and enriching previews [#provider-limits-and-enriching-previews]

Only some providers and models return sources through the AI SDK (for example Perplexity Sonar, some Grok variants). Others stream plain text with no **`source-url`** parts. When sources exist, their shape can differ by provider.

Source parts usually give you at least a **URL**. **`Citation`** accepts optional **`title`** and **`description`** for the hover card; the SDK does not fetch page metadata for you. To show real titles or snippets, call a link-preview or metadata service (or your own scraper) and merge the results into the objects you pass to **`citations`** before rendering.

API Reference [#api-reference]

Citation [#citation]

Root component. **Normalizes** **`citations`**, stores **citation source data** and **carousel** state in context for descendants, and wraps **[Hover Card](https://www.radix-ui.com/primitives/docs/components/hover-card#root)**.

<TypeTable
  type={{
  defaultOpen: {
    type: "boolean",
    description: "Uncontrolled initial open state.",
  },
  open: {
    type: "boolean",
    description: "Controlled open state.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description: "Called when the open state changes.",
  },
  openDelay: {
    type: "number",
    description:
      "The duration from when the mouse enters the trigger or content until the hover card opens.",
    default: "50ms",
  },
  closeDelay: {
    type: "number",
    description:
      "The duration from when the mouse leaves the trigger or content until the hover card closes.",
    default: "50ms",
  },
  citations: {
    type: "CitationSourceInput[]",
    description:
      "Source(s) to show. Each item requires url; title and description are optional for the preview.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically CitationTrigger and CitationContent (and carousel or item primitives inside the content).",
  },
}}
/>

CitationTrigger [#citationtrigger]

The **source chip** users see in the UI. Wraps **[Hover Card Trigger](https://www.radix-ui.com/primitives/docs/components/hover-card#trigger)** so hovering it opens the **preview** (`CitationContent`). Adjust the chip with **`label`**, **`showFavicon`**, and **`showSiteName`**.

<TypeTable
  type={{
  label: {
    type: "React.ReactNode",
    description:
      "Replaces the auto-derived site-name text on the default chip.",
  },
  showFavicon: {
    type: "boolean",
    default: "true",
    description: "Show the first source’s favicon on the default chip.",
  },
  showSiteName: {
    type: "boolean",
    default: "true",
    description:
      "Show site-name (or label) text. When false with favicon on, yields a favicon-only chip.",
  },
  className: {
    type: "string",
    description: "Merged with default chip styles.",
  },
}}
/>

CitationContent [#citationcontent]

Preview card that appears when the trigger is hovered and shows the source details; Wraps **[Hover Card Content](https://www.radix-ui.com/primitives/docs/components/hover-card#content)**.

<TypeTable
  type={{
  align: {
    type: '"start" | "center" | "end"',
    default: '"center"',
    description: "Alignment relative to the trigger.",
  },
  alignOffset: {
    type: "number",
    default: "0",
    description: "Pixel offset along the alignment axis.",
  },
  side: {
    type: '"top" | "right" | "bottom" | "left"',
    default: '"bottom"',
    description: "Preferred side of the trigger for the popover.",
  },
  sideOffset: {
    type: "number",
    default: "4",
    description: "Distance from the trigger in px.",
  },
  className: {
    type: "string",
    description: "Merged with default card shell styles.",
  },
}}
/>

CitationCarousel [#citationcarousel]

[**Carousel**](https://ui.shadcn.com/docs/components/radix/carousel) root for multi-source slides. **`setApi`** registers the API on **`Citation`** (for nav/index) and invokes any **`setApi`** you pass.

<TypeTable
  type={{
  orientation: {
    type: '"horizontal" | "vertical"',
    default: '"horizontal"',
    description:
      "Carousel axis; forwarded to Embla via the shared Carousel primitive.",
  },
  opts: {
    type: "object",
    description: "Optional Embla carousel options.",
  },
  plugins: {
    type: "EmblaPlugin | EmblaPlugin[]",
    description: "Optional Embla plugins array.",
  },
  setApi: {
    type: "(api: CarouselApi | undefined) => void",
    description:
      "Optional; invoked when the Embla instance is ready or cleared.",
  },
  className: {
    type: "string",
    description: "Classes on the carousel region wrapper.",
  },
}}
/>

CitationCarouselHeader [#citationcarouselheader]

Header row above the slide viewport; Usually contains a summary **`CitationSource`** and **`CitationCarouselPagination`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default includes horizontal padding and top padding.",
  },
}}
/>

CitationCarouselContent [#citationcarouselcontent]

Wraps **[Carousel Content](https://ui.shadcn.com/docs/components/radix/carousel#content)** and syncs viewport **height** to the **active** slide.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Classes on the flex track inside the overflow viewport.",
  },
}}
/>

CitationCarouselItem [#citationcarouselitem]

One carousel slide; **`index`** selects **`citations[index]`** and sets **`CitationItemContext`**. Wraps **[Carousel Item](https://ui.shadcn.com/docs/components/radix/carousel#item)**.

<TypeTable
  type={{
  index: {
    type: "number",
    description: "Zero-based index into Citation citations.",
  },
  className: {
    type: "string",
    description: "Merged with slide layout (includes self-start).",
  },
}}
/>

CitationCarouselPagination [#citationcarouselpagination]

Flex row for nav controls.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default flex row with gap for prev / index / next.",
  },
}}
/>

CitationCarouselPrev / CitationCarouselNext [#citationcarouselprev--citationcarouselnext]

Icon buttons for prev/next navigation; Standard **`button`** HTML attributes.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default Hugeicons arrows.",
  },
  className: {
    type: "string",
    description: "Merged with default circular button styles.",
  },
}}
/>

CitationCarouselIndex [#citationcarouselindex]

**`current / count`** from context (**1-based** current). **`span`** HTML attributes.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default tabular-nums muted text.",
  },
}}
/>

CitationItem [#citationitem]

Preview row as an **`<a>`**, containing the source details; **`href`** defaults to the citation URL. Default stack: **`h4`** title, **`p`** description, **`CitationSource`** footer.

<TypeTable
  type={{
  showTitle: {
    type: "boolean",
    default: "true",
    description: "Show the title block.",
  },
  showDescription: {
    type: "boolean",
    default: "true",
    description: "Show the description block.",
  },
  showSource: {
    type: "boolean",
    default: "true",
    description: "Show the footer source row.",
  },
  href: {
    type: "string",
    description: "Active or item-scoped citation url.",
  },
  className: {
    type: "string",
    description: "Default column layout with padding inside the hover card.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Replaces the default title, description, and source stack when set.",
  },
}}
/>

CitationSource [#citationsource]

Citation source chip with the favicon and site name.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default flex row with gap.",
  },
}}
/>

CitationFavicon [#citationfavicon]

Favicon of the citation site. **`src`** overrides **`faviconSrc`** from context.

<TypeTable
  type={{
  src: {
    type: "string",
    description: "Optional favicon URL; defaults from citation context.",
  },
  className: {
    type: "string",
    description: "Wrapper div classes (size defaults to icon scale).",
  },
}}
/>

CitationSiteName [#citationsitename]

Site name of the citation site.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default truncate text-primary.",
  },
}}
/>

CitationSourcesBadge [#citationsourcesbadge]

Chip with an optional overlapping favicon stack plus a **`{n} source(s)`** label. Use in **`CitationCarouselHeader`**, message action rows, or anywhere inside **`Citation`**.

<TypeTable
  type={{
  showFavicons: {
    type: "boolean",
    default: "true",
    description: "Show the stacked favicons before the label.",
  },
  label: {
    type: "React.ReactNode",
    description: "Optional; defaults to “N source(s)” from citation count.",
  },
  className: {
    type: "string",
    description: "Secondary rounded-full row; tight padding for the chip.",
  },
}}
/>

parseCitationUrl [#parsecitationurl]

Tolerant parse to a **`URL`**: trims the string and, when no **`http`/`https`** scheme is present, prefixes **`https://`** so bare hostnames resolve. Use for hostname extraction, favicon lookups, or custom **`CitationTrigger`** labels outside the component tree.

```ts
export function parseCitationUrl(urlStr: string): URL;
```

resolveCitationSource [#resolvecitationsource]

Maps one **`CitationSourceInput`** to **`ResolvedCitation`**—normalized **`url`**, optional **`title`** / **`description`**, derived **`siteName`**, and a **`faviconSrc`** URL. **`resolveCitationSources`** maps an array; **`Citation`** uses this pipeline when resolving the **`citations`** prop.

```ts noCollapse
export type CitationSourceInput = {
  url: string;
  title?: React.ReactNode;
  description?: React.ReactNode;
};

export type ResolvedCitation = {
  url: string;
  title: React.ReactNode | null;
  description: React.ReactNode | null;
  siteName: string;
  faviconSrc: string;
};

export function resolveCitationSource(
  input: CitationSourceInput,
): ResolvedCitation;

export function resolveCitationSources(
  inputs: CitationSourceInput[],
): ResolvedCitation[];
```
