Browse all components
Citation UI
Inline citation markers in generated text — numbered footnotes that reveal the source in a hover preview card.
Preview
The most capable models ship in two variants — one with additional safety measures, and one restricted to approved organizations. Treat a token stream as a UX concern rather than a progress hack .
Presets
Props
Agent promptClaude Code · Cursor · any agent
Add the Citation UI component from the Scrim UI registry to this project and use it in the configuration described below.
Citation UI — Inline citation markers in generated text — numbered footnotes that reveal the source in a hover preview card.
## 1. Install
```bash
npx shadcn@latest add @scrimui/citation-ui
```
This writes a single file to `components/ui/citation-ui.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/citation-ui to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { InlineCitation, CitationList } from "@/components/ui/citation-ui";
const CITATIONS = [
{ id: 1, title: "Claude Fable 5 and Mythos 5", url: "https://www.anthropic.com/news/claude-fable-5-mythos-5", domain: "anthropic.com", snippet: "…" },
{ id: 2, title: "Prompt engineering for streaming UIs", url: "https://example.com/streaming-prompts", domain: "example.com", snippet: "…" },
{ id: 3, title: "Why agents need human approval", url: "https://example.com/agent-approval", domain: "example.com", snippet: "…" },
];
<p>
The most capable models ship in two variants — one with{" "}
<InlineCitation citation={CITATIONS[0]} /> additional safety measures, and one
restricted to approved organizations <InlineCitation citation={CITATIONS[1]} />.
</p>
```
## 3. The configuration that matters
This is the component's "Inline markers" state — Numbered markers in prose. Hover or focus previews the source without leaving the answer.
These props were set deliberately and should be preserved as written:
- `layout` = "inline" — Layout
Every other prop is at its default; leave those off the call site rather than writing the default value out.
Reference: https://scrimui.dev/components/citation-uiFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/citation-uiComponent source
React + Tailwind component using shadcn/ui (button). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add button"use client";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type Citation = {
id: number;
title: string;
url: string;
domain?: string;
snippet?: string;
};
export type InlineCitationProps = {
citation: Citation;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
function domainFromUrl(url: string) {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
}
/* ------------------------------------------------------------------ */
/* InlineCitation */
/* ------------------------------------------------------------------ */
export function InlineCitation({ citation, className = "" }: InlineCitationProps) {
const [open, setOpen] = React.useState(false);
const host = citation.domain ?? domainFromUrl(citation.url);
return (
<span className={`relative inline-flex ${className}`}>
<Button variant="ghost" size="icon-sm"
type="button"
aria-expanded={open}
aria-label={`Source ${citation.id}: ${citation.title}`}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
className="min-h-6 min-w-6 mx-0.5 inline-flex h-[15px] w-[15px] translate-y-[-2px] items-center justify-center rounded-full bg-muted text-xs font-semibold text-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{citation.id}
</Button>
{open && (
<a
href={citation.url}
target="_blank"
rel="noreferrer noopener"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
className="absolute left-1/2 top-full z-30 mt-2 w-64 -translate-x-1/2 rounded-xl border border-border bg-card p-3 shadow-md"
>
<p className="text-sm font-medium leading-snug text-foreground">
{citation.title}
</p>
<p className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
<span className="truncate">{host}</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="11" height="11" className="shrink-0">
<path d="M7 17 17 7M7 7h10v10" />
</svg>
</p>
{citation.snippet && (
<p className="mt-1.5 text-sm leading-5 text-muted-foreground">
{citation.snippet}
</p>
)}
</a>
)}
</span>
);
}
/* ------------------------------------------------------------------ */
/* CitationList — numbered source list rendered under an answer */
/* ------------------------------------------------------------------ */
export function CitationList({
citations,
className = "",
linkable = true,
}: {
citations: Citation[];
className?: string;
/** False inside a card already wrapped in <a>, where nested anchors are invalid HTML. */
linkable?: boolean;
}) {
if (citations.length === 0) return null;
return (
<div className={`space-y-1.5 ${className}`}>
<p className="text-xs font-medium text-muted-foreground">Sources</p>
<ol className="space-y-1">
{citations.map((c) => {
const titleClass =
"truncate text-muted-foreground transition-colors hover:text-foreground hover:underline";
return (
<li key={c.id} className="flex items-baseline gap-2 text-sm">
<span className="w-4 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
{c.id}
</span>
{linkable ? (
<a
href={c.url}
target="_blank"
rel="noreferrer noopener"
className={titleClass}
>
{c.title}
</a>
) : (
<span className={titleClass}>{c.title}</span>
)}
</li>
);
})}
</ol>
</div>
);
}
When to use it
- Number sources in order of first citation so markers stay stable through the answer.
- Make the marker a real button/link — hover previews, click opens. Never a span with a title attribute.
- Keep hover cards above or below the text with enough offset they can't cover the next sentence.
- Provide the source list at the bottom — some users skip inline markers entirely.
- Keep snippets to two lines; the card should be glanceable, not a second article.
What breaks in production
- Dropping markers into streaming text before the citation exists — cite only what's already grounded.
- Making the hover card dismiss on mouse-leave between text and card — bridge the gap with padding.
- Rendering citations as plain superscript numbers with no affordance that they're links.
- Reusing the same number for different sources across the answer — users will assume the error is theirs.
Guides
Related Components
A cited source as a card — favicon, page title, domain, and the snippet the answer actually drew from.
The passages retrieval actually returned, with similarity scores and a visible relevance floor — including the case where nothing cleared it.
A citation marker that shows the passage it came from — fixed positioning that survives a scrolling answer, touch and keyboard, and an honest state for a number the model invented.
The message input at the heart of an AI chat app — file attachments, model picker, tool toggles, voice, and a send button that turns into stop.