PixelDosa UI

Inline Citations

Sources attached to individual claims, where checking takes one step. The supporting passage is shown beside the claim and links straight to that spot in the source.

Retention improved by six points in Q3, and the gain is concentrated in teams that used the guided setup checklist. The effect will likely hold through Q4.

Sources

  1. 1.Q3 2026 Retention Report · Analytics team, 4 Oct 2026
  2. 2.Onboarding survey results · Research, Sep 2026
  3. 3.Release notes 4.2 · Product

Installation

bash
npx shadcn@latest add @pixeldosa/inline-citations

Requires `cn` at `@/lib/utils`. Pass the exact retrieved passage as `quote` — that is what makes checking take one step. Set `support="paraphrased"` when the claim is the model's summary. Leave `source` off rather than attaching a loosely related source.

Usage

"use client";

import { Cite, CitedText } from "@/components/ui/inline-citations";

<CitedText sources={[{ id: "report", title: "Q3 Retention Report", url: "https://…" }]}>
  <p>
    <Cite source="report" quote="30-day retention rose from 41% to 47%.">
      Retention improved by six points in Q3
    </Cite>
    .
  </p>
</CitedText>

Why it exists

Research on AI answers keeps finding the same thing: people rarely check citations. A little number makes an answer look verified without anyone verifying it. So the goal isn't to show sources. It's to make checking one so cheap that people actually do it.

Features

  • The evidence comes to you. The popover shows the supporting passage next to the claim. No tab switch, no hunting through a PDF.
  • Open at this passage. The link uses a URL text fragment, so the source opens scrolled to the quoted words and highlighted, in browsers that support it.
  • Quoted or summarised. The popover says whether the source says this or the model summarised it.
  • Unsourced claims are marked. A <Cite> with no source shows a dashed ? and says so. An uncited sentence among cited ones otherwise borrows their credibility.
  • You can see what's being cited. The claim is highlighted while its source is open.
  • The reference list is always there, for readers without hover and for audits.

Examples

A claim backed by two sources, each in its own words:

<Cite
  source={["survey", "changelog"]}
  support="paraphrased"
  quote={{ survey: "Respondents who used the checklist…", changelog: "4.2 introduces a checklist…" }}
>
  used the guided setup checklist
</Cite>

Mapping retrieval results. Keep the retrieved chunk, because it is the quote:

const sources = retrieved.map((doc) => ({ id: doc.id, title: doc.title, url: doc.url }));

{answer.claims.map((claim) => (
  <Cite key={claim.id} source={claim.sourceIds} quote={claim.passage}>
    {claim.text}
  </Cite>
))}

Props

CitedText

PropTypeDescription
sources{ id, title, url?, publisher?, date? }[]Required. Numbered in this order.
showReferencesbooleanThe reference list. Defaults to true.
headingLevel2 | 3 | 4 | 5 | 6Heading level, so the component fits your page's outline. Defaults to 4.

Cite

PropTypeDescription
sourcestring | string[]Source id or ids. Omit to mark the claim unsourced.
quotestring | Record<id, string>The supporting passage, or one per source.
support"quoted" | "paraphrased"Defaults to "quoted".

Accessibility

  • Markers are buttons named "Source 1: Q3 Retention Report" or "No source for this claim".
  • The popover opens on keyboard focus, sits next to its marker in the DOM so Tab reaches its link, and closes with Escape, which returns focus to the marker.
  • On touch, tapping a marker opens it and tapping outside closes it.
  • The reference list is a real ordered list with ordinary links.

When not to use

For "why this?" about a whole answer, use AI Context Surface. For what the agent did to find things, use Tool Call Card. And don't attach a loosely related source to fill a gap. Leave source off.

why it's built this way

Engineering Notes

Designed from one research finding: people rarely verify citations, so a numbered marker is mostly a comfort signal that makes answers look more trustworthy than they are. The design makes the check cheap enough to actually happen. The supporting passage itself appears in the popover beside the claim, so comparing the two needs no navigation. The link opens the source at that passage using a URL text fragment, with start and end words for long quotes because fragments must match exactly. Support is a tier, not a score: 'quoted' vs 'paraphrased'. A claim without a source is marked with a dashed '?' rather than left bare, because an uncited sentence sitting among cited ones borrows their credibility. Numbering comes from the order of the `sources` array rather than render-order registration, so it is identical on server and client. The popover stays in the DOM next to its marker, so Tab moves from marker into the link. It is displayed with the Popover API's top layer and fixed coordinates, which keeps it clear of overflow-hidden containers and transformed ancestors. Without Popover API support it falls back to fixed positioning. It opens on mouse hover after a short delay, with a grace period so the pointer can move into it, and on keyboard focus-visible or tap. Clicking outside dismisses it, and Escape closes it and returns focus. The always-visible reference list is the fallback where hover doesn't exist and the audit view where it does.

how it moves, and when it doesn't

Motion Notes

Minimal, because reading is the user's turn. The popover fades in at `--pd-duration-fast` on the decelerate curve, with no scale or slide, so the quote is readable from its first frame. Hover opens after 180ms, so markers don't flash as the pointer crosses text, and closing waits 140ms so the pointer can travel into the popover. The claim highlight and the marker's active fill change colour at `--pd-duration-instant`. Nothing shimmers: citations describe finished content. All motion becomes instant under prefers-reduced-motion.