Inline citation
Numbered references inside a sentence, each with a hover preview of its source.
Optimistic updates keep the thread responsive while the server confirms the write. The store already exposes a consistent snapshot for every subscriber, so no extra reconciliation pass is needed.
Installation
npx shadcn@latest add "@assistant-ui/elements-inline-citation"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-inline-citation"Props-driven: no runtime or provider required.
Inline citation drops small numbered markers into a sentence; hovering one opens a preview card with the source's domain, title, and snippet. This element has no runtime composition: assistant-ui has no positional link between a citation marker and an offset inside streamed message text, so it ships as a standalone specimen and you adapt the sentence it wraps directly in the installed file.
Getting started
Render the citations
"use client";
import { useState } from "react";
import {
InlineCitation,
type Source,
} from "@/components/assistant-ui/elements/inline-citation";
const SOURCES: Source[] = [
{
domain: "assistant-ui.com",
title: "Optimistic updates in the runtime",
snippet:
"The runtime applies local edits immediately and reconciles them once the server acknowledges the write.",
},
{
domain: "react.dev",
title: "useSyncExternalStore reference",
snippet:
"Subscribes a component to an external store, re-rendering on every store change with a consistent snapshot.",
},
];
export function Answer() {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<InlineCitation
sources={SOURCES}
openIndex={openIndex}
onOpenIndexChange={setOpenIndex}
/>
);
}This renders the element's own fixed sentence with markers 1 and 2 attached at its two anchor points, previewing SOURCES[0] and SOURCES[1].
Replace the fixed sentence
The paragraph text is written directly into the source, not passed as a prop. Edit the installed file to wrap your own sentence and place <Citation index={n} source={...} open={...} onOpenChange={...} /> wherever a claim needs one:
<p data-slot="inline-citation" className={cn("...", className)} {...props}>
Revenue grew 12 percent year over year
{sources[0] && (
<Citation
index={0}
source={sources[0]}
open={openIndex === 0}
onOpenChange={(open) => onOpenIndexChange(open ? 0 : null)}
/>
)}
, driven mostly by the enterprise tier.
</p>Anatomy
<p data-slot="inline-citation">
{/* fixed sentence text, with a numbered marker after each of two anchor points */}
</p>The sentence itself never changes: only which of its two built-in anchor points gets a marker depends on sources. Passing zero or one source omits the corresponding marker (sources[0] && and sources[1] && guard each one); passing more than two sources, the rest have nowhere to attach and are simply unused. openIndex admits only one open preview at a time: setting it to 0 implies index 1 is closed, since each Citation's open reads openIndex === <its own index>. Passing null closes every marker.
Examples
Restyle the markers
The root takes className. Each marker and its preview popup read the shared floating surface and mono tokens from surfaces.tsx.
<InlineCitation className="max-w-none" /* ... */ />Keeping previews mutually exclusive across citations
Because openIndex is a single value, opening one marker already closes any other. A "close on scroll" or "close on send" affordance only needs to reset it once:
useEffect(() => {
const onScroll = () => setOpenIndex(null);
window.addEventListener("scroll", onScroll, true);
return () => window.removeEventListener("scroll", onScroll, true);
}, []);API reference
InlineCitation
| Prop | Type | Default | Description |
|---|---|---|---|
sources | Source[] | required | Citation targets. Only indices 0 and 1 are ever attached to a marker. |
openIndex | number | null | required | Which citation's preview is open. null closes every marker. |
onOpenIndexChange | (index: number | null) => void | required | Called with the marker's index when it opens, or null when it closes. |
className | string | Merged onto the root. |
Source is { domain: string; title: string; snippet: string }. All other p props are forwarded to the root.