--- title: Citation description: Inline source references with hover preview --- 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"; 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. ## Installation ```bash npx shadcn@latest add @nexus-ui/citation ``` ```bash pnpm dlx shadcn@latest add @nexus-ui/citation ``` ```bash yarn dlx shadcn@latest add @nexus-ui/citation ``` ```bash bunx shadcn@latest add @nexus-ui/citation ```

Copy and paste the following code into your project.

Install registry dependencies: @nexus-ui/nexus-ui-theme, carousel, and hover-card (or use the CLI so they install automatically).

Update the import paths to match your project setup.

## Usage ```tsx keepBackground noCollapse import { Citation, CitationContent, CitationItem, CitationTrigger, } from "@/components/nexus-ui/citation"; ``` ```tsx keepBackground noCollapse ``` `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 ### 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. ### 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. ### Inline with text The trigger can sit **inline** with surrounding copy—place **`Citation`** where a phrase or sentence needs a source chip. ## Vercel AI SDK Integration The [Vercel AI SDK](https://sdk.vercel.ai) does not define a single standard for **inline citations** in chat streams—models are not required to emit a shared citation syntax, and there is no built-in “citation” channel you can rely on for every provider. [Streamdown](https://streamdown.ai/) also has **no official inline-citation or footnote-to-chip resolution**; you have to wire sources yourself. **Citation** takes **`citations: CitationSourceInput[]`** only. In practice, you can fill that array from **(1)** a **structured object** stream (**`streamObject`** + **`experimental_useObject`**) with a Zod schema, or **(2)** assistant **`UIMessage`** parts of **`type: "source-url"`** from **`useChat`** when your **`POST /api/chat`** response actually includes them (**`toUIMessageStreamResponse({ sendSources: true })`** and a model or provider that emits source parts—many completions only stream **`text`**). ### Structured object stream

Install the AI SDK and Zod

```bash npm install ai @ai-sdk/react @ai-sdk/openai zod ```

Add the citation API route

Define the Zod schema in this file and export it so the client can pass the same shape to **`useObject`**. ```ts title="app/api/citation/route.ts" import { streamObject, zodSchema } from "ai"; import { openai } from "@ai-sdk/openai"; import { z } from "zod"; export const citationSchema = z.object({ content: z.string(), citations: z.array( z.object({ number: z.string(), title: z.string(), url: z.string(), description: z.string().optional(), }), ), }); export const maxDuration = 30; export async function POST(req: Request) { const { prompt } = (await req.json()) as { prompt: string }; const result = streamObject({ model: openai("gpt-4o"), schema: zodSchema(citationSchema), prompt: `Generate a well-researched paragraph about ${prompt} with proper citations. Include: - A comprehensive paragraph with inline citations marked as [1], [2], etc. - 2-3 citations with realistic source information - Each citation should have a title, URL, and optional description - Make the content informative and the sources credible Format citations as numbered references within the text.`, }); return result.toTextStreamResponse(); } ```

Add a client page for structured citations

```tsx title="app/citations-object/page.tsx" "use client"; import { experimental_useObject as useObject } from "@ai-sdk/react"; import { Button } from "@/components/ui/button"; import { Citation, CitationContent, CitationItem, CitationTrigger, } from "@/components/nexus-ui/citation"; import { citationSchema } from "@/app/api/citation/route"; export default function CitationsObjectPage() { const { object, submit, isLoading } = useObject({ api: "/api/citation", schema: citationSchema, }); return (
{isLoading && !object ? (

Generating content with citations…

) : null} {object?.content ? (

{object.content.split(/(\[\d+\])/).map((part, index) => { const citationMatch = part.match(/\[(\d+)\]/); if (citationMatch) { const citationNumber = citationMatch[1]; const row = object.citations?.find( (c) => c.number === citationNumber, ); if (row) { return ( ); } } return part; })}

) : null}
); } ```
### Chat stream with source-url parts

Install the AI SDK

```bash npm install ai @ai-sdk/react @ai-sdk/openai ```

Add the chat API route

```ts title="app/api/chat/route.ts" import { convertToModelMessages, streamText, type UIMessage } from "ai"; import { openai } from "@ai-sdk/openai"; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai("gpt-4o-mini"), system: "You are a helpful assistant. When you cite web sources, include URLs in your answer where applicable.", messages: await convertToModelMessages(messages), }); return result.toUIMessageStreamResponse({ sendSources: true, }); } ```

Add a client page for chat citations

```tsx title="app/citations-chat/page.tsx" "use client"; import { useCallback, useMemo, useState } from "react"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport, isTextUIPart, type UIMessage, } from "ai"; import { Button } from "@/components/ui/button"; import { Citation, CitationContent, CitationItem, CitationTrigger, type CitationSourceInput, } from "@/components/nexus-ui/citation"; function textFromMessage(message: UIMessage) { return message.parts.filter(isTextUIPart).map((p) => p.text).join(""); } function citationsFromSourceUrlParts( message: UIMessage, ): CitationSourceInput[] { return message.parts .filter( (p): p is Extract => p.type === "source-url", ) .map((p) => ({ url: p.url, title: p.title, })); } export default function CitationsChatPage() { const [input, setInput] = useState(""); const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat" }), }); const lastAssistant = useMemo( () => [...messages].reverse().find((m) => m.role === "assistant"), [messages], ); const citations = lastAssistant ? citationsFromSourceUrlParts(lastAssistant) : []; const assistantText = lastAssistant ? textFromMessage(lastAssistant) : ""; const isBusy = status === "submitted" || status === "streaming"; const handleSubmit = useCallback(() => { const t = input.trim(); if (!t) return; sendMessage({ text: t }); setInput(""); }, [input, sendMessage]); return (