Elements

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.

fig. 01

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 init

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

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

app/answer.tsx
"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:

components/assistant-ui/elements/inline-citation.tsx
<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

PropTypeDefaultDescription
sourcesSource[]requiredCitation targets. Only indices 0 and 1 are ever attached to a marker.
openIndexnumber | nullrequiredWhich citation's preview is open. null closes every marker.
onOpenIndexChange(index: number | null) => voidrequiredCalled with the marker's index when it opens, or null when it closes.
classNamestringMerged onto the root.

Source is { domain: string; title: string; snippet: string }. All other p props are forwarded to the root.