Skip to content
Scrim UI

Citation UI

Inline citation markers in generated text — numbered footnotes that reveal the source in a hover preview card.

citationinlinereference

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-ui

Follows 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-ui

Component 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
citation-ui.tsx
"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