Citation UI UI
Inline citation markers with hover preview cards.
citationinlinereference
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 .
Props
Component source
Single-file React + Tailwind component. No dependencies — drop it into any project with Tailwind configured.
citation-ui.tsx
"use client";
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
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="mx-0.5 inline-flex h-[15px] w-[15px] translate-y-[-2px] items-center justify-center rounded-full bg-zinc-200 text-[9px] font-semibold text-zinc-700 transition-colors hover:bg-zinc-900 hover:text-white dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-100 dark:hover:text-zinc-900"
>
{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-zinc-200 bg-white p-3 shadow-lg dark:border-zinc-700 dark:bg-zinc-800"
>
<p className="text-sm font-medium leading-snug text-zinc-800 dark:text-zinc-100">
{citation.title}
</p>
<p className="mt-1 flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400">
<span className="truncate">{host}</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" 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-[13px] leading-5 text-zinc-500 dark:text-zinc-400">
{citation.snippet}
</p>
)}
</a>
)}
</span>
);
}
/* ------------------------------------------------------------------ */
/* CitationList — numbered source list rendered under an answer */
/* ------------------------------------------------------------------ */
export function CitationList({
citations,
className = "",
}: {
citations: Citation[];
className?: string;
}) {
if (citations.length === 0) return null;
return (
<div className={`space-y-1.5 ${className}`}>
<p className="text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Sources</p>
<ol className="space-y-1">
{citations.map((c) => (
<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-zinc-500 dark:text-zinc-400">
{c.id}
</span>
<a
href={c.url}
target="_blank"
rel="noreferrer noopener"
className="truncate text-zinc-600 transition-colors hover:text-zinc-900 hover:underline dark:text-zinc-300 dark:hover:text-zinc-100"
>
{c.title}
</a>
</li>
))}
</ol>
</div>
);
}
Usage Guidelines
- 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.
Common UX Mistakes
- 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.
Related Components
Source Card
A cited source with favicon, title, domain and snippet.
Prompt Input
The core input for AI chat products — with attachments, model selector, tools, voice and loading states.
Prompt Input + Attachments
Prompt input with file attachment chips, upload progress and removal.
Prompt Input + Model Selector
Prompt input with an inline model picker and capability badges.