Skip to content
KONIGI Patterns

AI Assistants / Understanding answers / Source list

‹ 4 of 5 ›

Source list

Readers need to review the sources used to produce an answer.

Updated September 12, 2026

Problem

People need to judge the sources behind an answer without opening every citation in the text.

Solution

List the sources used for the answer, with a title, publisher or domain, and date where available. Link to each source and show known access restrictions before people open it.

Place the list where it supports the task. A compact preview near the answer can help readers assess factual information; a longer list can sit below it or behind an expansion control.

Distinguish sources used in the answer from other search results. Do not imply that every retrieved page supports the response. Keep the visible list short enough to scan, with the rest available on request.

Use when

An answer uses information retrieved from websites, files, or an internal collection.

Don’t use when

The answer is generated from the viewer’s own supplied text or from the model’s parameters alone. A list of sources under a rewrite of the viewer’s paragraph is theatre.

Trade-offs

Detailed source lists take space and can delay the answer people came for. A long list may look more convincing without providing stronger evidence. Inaccessible sources limit what readers can verify.

Checklist

  • Is the list visible before the answer is read, or only after?
  • Does each row carry a domain and a date without a click?
  • Can the reader tell what was retrieved against what was actually used?
  • Are paywalled, dead, and permission-restricted sources marked before the click?
  • How many rows show before an expander, and what’s the ordering?
  • Does the ordering reflect how much each source contributed, or arrival order?
  • What does the list look like when retrieval returned nothing good?
  • Does the answer say when it went beyond its sources?
  • Is the list included when the answer is copied or shared?
  • Do the rows survive a narrow viewport without truncating the domain?

Compare

Perplexity puts the sources above the prose as a horizontal strip and treats them as the primary object. The answer reads as a synthesis of a visible corpus rather than an opinion with footnotes. ChatGPT attaches sources when a search ran and keeps them subordinate to the answer, preserving the assistant framing at the cost of later calibration. Claude surfaces project knowledge and attached documents as the basis, so the list describes a corpus the viewer assembled rather than one the system found. Microsoft Copilot lists internal files from the tenant, where a row’s real information is often whether the reader can open it at all.

Citation chip links a particular claim to a source. Tool call trace records the search or lookup. Attachment tray contains files supplied by the user.

Scoped context defines where the assistant can look. Reasoning disclosure summarizes its approach to the task.

Source list anatomy A row of sources placed above the answer rather than below it, each carrying a domain and a date, one marked as paywalled and one as permission-restricted, with a line stating how many candidates were retrieved against how many were used. Before the answer, so calibration comes first prometheus.io Jan 2026 nngroup.com Apr 2026 wsj.com paywalled finance/Q3.xlsx no access retrieved 20, used 4 the answer, read with the sources already in mind 1 3 2 4 1 DOMAIN DOES THE WORK A standards body, a vendor blog and a forum post are told apart instantly. 2 RETRIEVED VERSUS USED Showing all twenty pads the rigour. Showing four hides how thin the basis was. 3 UNREACHABLE SOURCES Mark a paywall or a permission wall before the click, not after it fails. 4 ABOVE, NOT BELOW Calibration is only useful in advance. A bibliography is read by almost nobody. Once a product shows its sources it gets judged on them, which is the right pressure to accept.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Sources placed above the answer so calibration comes first. Each carries a domain and a date, a paywalled one and an inaccessible one are marked, and the count says how many candidates were retrieved against how many were used.

shadcn
npx shadcn@latest add button
npm
lucide-react
Tokens
--card-foreground--muted--muted-foreground--border--status-warn--chart-1

retrieved 20, used 4

Report latency at p50, p95 and p99 rather than as a mean, since the tail is what pages someone, and put the comparison window beside every figure on the tile. The sector benchmark is taken from the WSJ piece's summary only, and the Q3 spreadsheet could not be read, so the infrastructure line below is from last quarter's figures.

SourceList.tsxRows with a domain, a date and a closed access state, the retrieved-versus-used count, and the answer as children so it always renders below.

import { useState, type ReactNode } from "react";
import { Lock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

/**
 * A row is judged before the click, so the things that decide the judgement
 * are required: the domain, the date, and whether the reader can open it at
 * all. `access` is a closed set because each value wants a different mark and
 * a different explanation, and "unavailable" would cover none of them.
 */
export type Access = "open" | "paywalled" | "no-access" | "dead";

export type Source = {
  title: string;
  /** Host, or a path for a document in the viewer's own corpus. */
  domain: string;
  /** As the source states it: "Jan 2026". */
  date: string;
  url: string;
  access: Access;
};

const ACCESS_LABEL: Record<Exclude<Access, "open">, string> = {
  paywalled: "paywalled",
  "no-access": "no access",
  dead: "link dead",
};

export function SourceList({ sources, retrieved, max = 8, onOpen, children }: {
  /** What the answer drew on, most-used first. */
  sources: Source[];
  /** How many candidates retrieval returned before generation chose. Required:
   *  the used count alone hides how thin the basis was. */
  retrieved: number;
  /** Rows shown before the expander. */
  max?: number;
  onOpen?: (source: Source) => void;
  /** The answer. It renders below the list, so the reader is calibrated first. */
  children: ReactNode;
}) {
  const [expanded, setExpanded] = useState(false);
  const shown = expanded ? sources : sources.slice(0, max);
  const used = sources.length;

  return (
    <div>
      <ul className="grid grid-cols-2 gap-3 sm:grid-cols-4" aria-label="sources">
        {shown.map((s) => {
          const gated = s.access !== "open";
          return (
            <li key={s.url}>
              <a
                href={s.url}
                target="_blank"
                rel="noreferrer"
                onClick={() => onOpen?.(s)}
                title={s.title}
                className={cn("block rounded-md border px-3 py-2 no-underline", gated && "border-status-warn")}
              >
                <p className="truncate text-[11px] text-card-foreground">{s.domain}</p>
                <p className={cn("mt-1 flex items-center gap-1 text-[11px]", gated ? "text-status-warn" : "text-muted-foreground")}>
                  {gated && <Lock className="size-2.5" aria-hidden="true" />}
                  {gated ? ACCESS_LABEL[s.access as Exclude<Access, "open">] : s.date}
                </p>
              </a>
            </li>
          );
        })}
      </ul>

      <div className="mt-4 flex items-center gap-4">
        <p className="text-[11px] tabular-nums text-muted-foreground">retrieved {retrieved}, used {used}</p>
        <span role="meter" aria-valuemin={0} aria-valuemax={retrieved} aria-valuenow={used} aria-label="sources used of those retrieved"
          className="flex h-2 w-[200px] overflow-hidden rounded-full bg-muted">
          <span className="bg-chart-1" style={{ width: `${(100 * used) / retrieved}%` }} />
        </span>
        {sources.length > max && (
          <Button variant="link" size="sm" className="ml-auto h-auto p-0 text-[11px]" onClick={() => setExpanded((e) => !e)}>
            {expanded ? "fewer" : `${sources.length - max} more`}
          </Button>
        )}
      </div>

      <div className="mt-4 border-t pt-4">{children}</div>
    </div>
  );
}

demo.tsxHow it is called: four sources used of twenty retrieved, one paywalled and one the viewer cannot open, above the answer.

import { SourceList, type Source } from "./SourceList";

/**
 * Four sources used out of twenty retrieved, above the answer they produced.
 * Two are open, one is behind a paywall, and the figures spreadsheet is one
 * the viewer cannot open, which the answer then has to admit.
 */
const SOURCES: Source[] = [
  { title: "Histograms and summaries", domain: "prometheus.io", date: "Jan 2026", url: "https://prometheus.io/docs/practices/histograms/", access: "open" },
  { title: "Dashboards: making charts and graphs easier to understand", domain: "nngroup.com", date: "Apr 2026", url: "https://www.nngroup.com/articles/dashboards-preattentive/", access: "open" },
  { title: "Cloud spending rises as software firms chase AI capacity", domain: "wsj.com", date: "Aug 2026", url: "https://www.wsj.com/tech/ai/cloud-spending-ai-capacity", access: "paywalled" },
  { title: "Q3 figures", domain: "finance/Q3.xlsx", date: "Sep 2026", url: "drive://finance/Q3.xlsx", access: "no-access" },
];

export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4">
      <SourceList sources={SOURCES} retrieved={20} onOpen={(s) => console.log("opened", s.domain)}>
        <p className="text-sm leading-relaxed text-card-foreground">
          Report latency at p50, p95 and p99 rather than as a mean, since the tail is what pages someone, and put the comparison window beside every figure on the tile. The sector benchmark is taken from the WSJ piece's summary only, and the Q3 spreadsheet could not be read, so the infrastructure line below is from last quarter's figures.
        </p>
      </SourceList>
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No screenshots yet for this pattern. Browse Products to see other examples.