17 / 37 · Knowledge
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"
Usage
import { InlineCitation } from "@/components/elements/inline-citation";
<InlineCitation
sources={[
{
domain: "docs.example.com",
title: "Optimistic updates",
snippet: "Confirm writes after paint.",
},
]}
openIndex={0}
onOpenIndexChange={setOpenIndex}
/>Props
sources*Source[]Sources attached to the numbered reference markers in the sentence.
openIndex*number | nullWhich citation preview is open, or null when none is open.
onOpenIndexChange*(index: number | null) => voidCalled when a citation marker opens or closes its preview.
classNamestringExtra classes merged onto the root.
Source
inline-citation.tsx"use client";
import { PreviewCard } from "@base-ui/react/preview-card";
import { cn } from "@/lib/utils";
import { floating, mono } from "./surfaces";
export interface Source {
domain: string;
title: string;
snippet: string;
}
interface CitationProps {
index: number;
source: Source;
open: boolean;
onOpenChange: (open: boolean) => void;
}
function Citation({ index, source, open, onOpenChange }: CitationProps) {
return (
<PreviewCard.Root open={open} onOpenChange={onOpenChange}>
<PreviewCard.Trigger
delay={0}
render={<button type="button" />}
className={cn(
"mx-0.5 inline-flex h-4 min-w-4 translate-y-[-2px] cursor-default items-center justify-center rounded-[5px] px-1 align-middle font-mono text-[10px] font-medium tabular-nums transition-colors",
open
? "bg-foreground text-background"
: "bg-foreground/[0.06] text-foreground/45 hover:text-foreground/90",
)}
>
{index + 1}
</PreviewCard.Trigger>
<PreviewCard.Portal>
<PreviewCard.Positioner side="top" sideOffset={8}>
<PreviewCard.Popup
className={cn(
floating,
"z-50 w-64 origin-(--transform-origin) rounded-2xl p-3.5 outline-none",
"transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
"data-[starting-style]:scale-[0.97] data-[starting-style]:opacity-0",
"data-[ending-style]:scale-[0.97] data-[ending-style]:opacity-0",
)}
>
<div className="flex items-center gap-1.5">
<span className="bg-foreground/[0.06] text-foreground/45 flex size-4 items-center justify-center rounded text-[9px] font-medium">
{source.domain[0]?.toUpperCase()}
</span>
<span className={cn(mono, "text-foreground/40")}>
{source.domain}
</span>
</div>
<p className="mt-2 text-[13px] leading-snug font-medium">
{source.title}
</p>
<p className="text-foreground/50 mt-1 text-[13px] leading-relaxed">
{source.snippet}
</p>
</PreviewCard.Popup>
</PreviewCard.Positioner>
</PreviewCard.Portal>
</PreviewCard.Root>
);
}
export interface InlineCitationProps {
sources: Source[];
openIndex: number | null;
onOpenIndexChange: (index: number | null) => void;
className?: string;
}
export function InlineCitation({
sources,
openIndex,
onOpenIndexChange,
className,
}: InlineCitationProps) {
return (
<p
className={cn(
"text-foreground/90 max-w-sm text-sm leading-relaxed",
className,
)}
>
Optimistic updates keep the thread responsive while the server confirms
the write
{sources[0] && (
<Citation
index={0}
source={sources[0]}
open={openIndex === 0}
onOpenChange={(open) => onOpenIndexChange(open ? 0 : null)}
/>
)}
. The store already exposes a consistent snapshot for every subscriber
{sources[1] && (
<Citation
index={1}
source={sources[1]}
open={openIndex === 1}
onOpenChange={(open) => onOpenIndexChange(open ? 1 : null)}
/>
)}
, so no extra reconciliation pass is needed.
</p>
);
}