Skip to content

Source cards

Retrieved context as cards, with inline citation markers that point at them.

Status
Beta
Category
AI
Adoption
Not used yet
import { SourceCards } from "@oration/canon/components/ai/source-cards";
packages/canon/src/components/ai/source-cards.tsx

Suggested answer

The payment went out by ACH on Friday, September 25. Orchard Street said their bank hasn't seen it, so share the trace number 091000019284113 and ask them to give it to their bank, which usually finds the deposit within a business day.

Sources3

  1. 1Payments playbook92% matchTracing an ACH the supplier hasn't receivedShare the 15-digit trace number and the settlement date. Most banks locate the deposit within one business day once they have the trace.
  2. 2ERP payment89% matchPayment PMT-58177ACH, $8,940.00 to Orchard Street, sent Friday, Sep 25. Trace 091000019284113.
  3. 3Call on Sep 2481% matchCall with Orchard Street accounts payable“We still don't see the September 25 payment on our side, and our bank says nothing came in.”
import { Citation, type Source, SourceCards, SourcesProvider } from "@oration/canon/components/ai/source-cards";import { SparklesIcon } from "lucide-react";export function Hero() {    const sources: Source[] = [        {            id: "kb-ach-trace",            kind: "doc",            title: "Tracing an ACH the supplier hasn't received",            snippet:                "Share the 15-digit trace number and the settlement date. Most banks locate the deposit within one business day once they have the trace.",            meta: "Payments playbook",            score: 0.92,        },        {            id: "pmt-58177",            kind: "record",            title: "Payment PMT-58177",            snippet:                "ACH, $8,940.00 to Orchard Street, sent Friday, Sep 25. Trace 091000019284113.",            meta: "ERP payment",            score: 0.89,        },        {            id: "call-sep24",            kind: "transcript",            title: "Call with Orchard Street accounts payable",            snippet:                "“We still don't see the September 25 payment on our side, and our bank says nothing came in.”",            meta: "Call on Sep 24",            score: 0.81,        },    ];    return (        <SourcesProvider sources={sources}>            <div className="flex w-full max-w-xl flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border">                <h3 className="flex items-center gap-1.5 text-13 font-medium text-foreground">                    <SparklesIcon                        aria-hidden="true"                        className="size-3.5 text-muted-foreground"                    />                    Suggested answer                </h3>                <p className="text-sm leading-relaxed text-pretty text-foreground">                    The payment went out by ACH on Friday, September 25                    <Citation index={2} />. Orchard Street said their bank                    hasn't seen it                    <Citation index={3} />, so share the trace number                    091000019284113 and ask them to give it to their bank, which                    usually finds the deposit within a business day                    <Citation index={1} />.                </p>                <SourceCards sources={sources} />            </div>        </SourcesProvider>    );}

Usage#

Source cards show the context an answer was built from: knowledge base passages, supplier and payment records, call transcripts and web pages. Each card is a tint well with a number, the source's kind and origin, an optional match score, a title and a two-line snippet. Inline Citation markers in the text carry the same numbers, and hovering either side highlights the other. The mistake to avoid is sources with no markers: the point is that each claim in the answer points at the card that supports it.

When to use

  • Under an AI answer or summary, in a scrolling row, with [n] markers in the text.
  • In an assist or retrieval panel, as a list, when the sources are the result (From the knowledge base).
  • In a wider panel as a grid when there are many sources to compare.
  • As a single SourceCard when one piece of context is pinned beside a draft.
  • Through StreamingText's citations prop, which mounts the provider, markers and cards for you.

When not to use

  • For files a person attached to a message. Use Attachment
  • For a summary that streams in with its sources. Pass citations and let it compose the cards. Use Streaming text
  • To preview a record behind an ordinary link. Use Hover card
  • For search results a person browses and opens, with actions per row. Use Item
  • For a generated insight with a metric and chart. Use Insight card

The Tint Well Rule

A source card is a 70% Well Gray region with 10px corners, not a bordered or shadowed card. It can sit inside a message, a panel or a card without nesting cards.

The Tabular Figures Rule

Citation numbers, card numbers and match scores are tabular, so a row of 1 2 3 markers and 92% scores line up.

Anatomy#

The payment went out on Friday.

1ERP payment89% matchPayment PMT-58177ACH, $8,940.00 to Orchard Street, sent Friday, Sep 25. Trace 091000019284113.
  1. Citation. An 11px number in a 16px Well Gray chip, raised a third of a line. Hover or focus previews the source and highlights its card.
  2. Number. The card's position, in a white chip with a hairline lift, matching the citation.
  3. Kind icon. 14px: a document, a globe for web pages, a database for records, audio lines for call transcripts.
  4. Origin. meta in 12px Slate Meta, such as Payments playbook or Call on Sep 24. Falls back to the kind's name.
  5. Score. Optional retrieval match at the right, 92%, announced as 92% match.
  6. Title. 13px medium, one line.
  7. Snippet. 12px Slate Meta, two lines.

Examples#

Layouts

Row scrolls sideways under a message and fades its clipped edge. Grid fills a wide panel. List stacks in a narrow one.

Sources4

  1. 1Knowledge base94% matchPayment run schedule, Q4ACH runs every Tuesday and Friday. Wires release the same business day before 2 PM CT.
  2. 2Payments playbook90% matchApproval cutoffsInvoices approved by 5:00 PM CT the day before a run are included in it.
  3. 3Supplier record86% matchNorthwind Freight vendor recordPaid by ACH. Terms net 30. Remittance to ar@northwindfreight.example.
  4. 4Call on Sep 2178% matchCall with Northwind Freight“If it's approved today, does it make Tuesday's run?”
import { type Source, SourceCards } from "@oration/canon/components/ai/source-cards";import { SegmentedControl } from "@oration/canon/components/segmented-control";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function Layouts() {    const [layout, setLayout] = React.useState<"row" | "grid" | "list">("row");    const sources: Source[] = [        {            id: "kb-runs",            kind: "doc",            title: "Payment run schedule, Q4",            snippet:                "ACH runs every Tuesday and Friday. Wires release the same business day before 2 PM CT.",            meta: "Knowledge base",            score: 0.94,        },        {            id: "kb-cutoff",            kind: "doc",            title: "Approval cutoffs",            snippet:                "Invoices approved by 5:00 PM CT the day before a run are included in it.",            meta: "Payments playbook",            score: 0.9,        },        {            id: "vendor-northwind",            kind: "record",            title: "Northwind Freight vendor record",            snippet:                "Paid by ACH. Terms net 30. Remittance to ar@northwindfreight.example.",            meta: "Supplier record",            score: 0.86,        },        {            id: "call-sep21",            kind: "transcript",            title: "Call with Northwind Freight",            snippet: "“If it's approved today, does it make Tuesday's run?”",            meta: "Call on Sep 21",            score: 0.78,        },    ];    return (        <div className="flex w-full flex-col gap-4">            <SegmentedControl                label="Layout"                value={layout}                onValueChange={setLayout}                options={[                    { value: "row", label: "Row" },                    { value: "grid", label: "Grid" },                    { value: "list", label: "List" },                ]}                className="self-start"            />            <div className={cn("w-full", layout === "list" && "max-w-sm")}>                <SourceCards sources={sources} layout={layout} />            </div>        </div>    );}

Kinds

Documents, web pages, records and call transcripts, each with its own icon. meta replaces the kind name with the source's origin. Web pages with an href open in a new tab.

Context used4

  1. 1Knowledge baseRemittance advice FAQDiscounts appear as a separate line on the remittance advice, not on the payment itself.
  2. 2Web pageACH returns and reason codesR01 means insufficient funds; R03 means no account or unable to locate the account.(opens in a new tab)
  3. 3Supplier recordHalcyon termsPayment terms 2/10 net 30. Early payment enrolled March 2026.
  4. 4Call on Sep 24Call with Halcyon accounts receivable“The $412.50 short on INV-20931, is that the early-pay discount?”
import { type Source, SourceCards } from "@oration/canon/components/ai/source-cards";export function Kinds() {    const sources: Source[] = [        {            id: "k-doc",            kind: "doc",            title: "Remittance advice FAQ",            snippet:                "Discounts appear as a separate line on the remittance advice, not on the payment itself.",            meta: "Knowledge base",        },        {            id: "k-url",            kind: "url",            title: "ACH returns and reason codes",            snippet:                "R01 means insufficient funds; R03 means no account or unable to locate the account.",            href: "https://example.com/ach-return-codes",        },        {            id: "k-record",            kind: "record",            title: "Halcyon terms",            snippet:                "Payment terms 2/10 net 30. Early payment enrolled March 2026.",            meta: "Supplier record",        },        {            id: "k-transcript",            kind: "transcript",            title: "Call with Halcyon accounts receivable",            snippet:                "“The $412.50 short on INV-20931, is that the early-pay discount?”",            meta: "Call on Sep 24",        },    ];    return (        <div className="w-full max-w-sm">            <SourceCards sources={sources} layout="list" title="Context used" />        </div>    );}

Citation markers

Hover or focus a marker to preview its source and highlight its card. Pass source when there is no provider. A marker with no source falls back to plain [n].

Halcyon was paid $20,212.50 on September 25, which is $412.50 less than the invoice because of the 2% early-payment discount on their terms. A third marker with no source falls back to plain text[3].

  1. 1ERP payment96% matchPayment PMT-58213ACH, $20,212.50, sent Sep 25. Discount applied: $412.50 (2/10 net 30).
  2. 2Supplier record89% matchHalcyon termsPayment terms 2/10 net 30. Early payment enrolled March 2026.
import { Citation, type Source, SourceCards } from "@oration/canon/components/ai/source-cards";export function Citations() {    const payment: Source = {        id: "c-pmt",        kind: "record",        title: "Payment PMT-58213",        snippet:            "ACH, $20,212.50, sent Sep 25. Discount applied: $412.50 (2/10 net 30).",        meta: "ERP payment",        score: 0.96,    };    const terms: Source = {        id: "c-terms",        kind: "record",        title: "Halcyon terms",        snippet:            "Payment terms 2/10 net 30. Early payment enrolled March 2026.",        meta: "Supplier record",        score: 0.89,    };    return (        <div className="flex w-full max-w-lg flex-col gap-4">            <p className="text-sm leading-relaxed text-pretty text-foreground">                Halcyon was paid $20,212.50 on September 25                <Citation index={1} source={payment} />, which is $412.50 less                than the invoice because of the 2% early-payment discount on                their terms                <Citation index={2} source={terms} />. A third marker with no                source falls back to plain text                <Citation index={3} />.            </p>            <SourceCards sources={[payment, terms]} title={null} />        </div>    );}

One pinned source

SourceCard on its own, without a number, for a piece of context pinned beside a draft.

Reply to Jordan Lee

Pinned for this reply

Payment securityBank change verificationBank changes need a verified callback to the number on file before the change is applied. Never use a number the caller gives you.
import { type Source, SourceCard } from "@oration/canon/components/ai/source-cards";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";export function SingleCard() {    const source: Source = {        id: "pinned",        kind: "doc",        title: "Bank change verification",        snippet:            "Bank changes need a verified callback to the number on file before the change is applied. Never use a number the caller gives you.",        meta: "Payment security",    };    return (        <div className="flex w-full max-w-md flex-col gap-2 rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex items-center justify-between gap-2">                <h3 className="text-13 font-medium text-foreground">                    Reply to Jordan Lee                </h3>                <Button                    type="button"                    variant="ghost"                    size="xs"                    onClick={() =>                        toast.add({                            title: "Source unpinned",                            description: "Bank change verification",                        })                    }                >                    Unpin                </Button>            </div>            <p className="text-13 text-muted-foreground">                Pinned for this reply            </p>            <SourceCard source={source} />        </div>    );}

In an assist panel

When the sources are the result, the list layout with a named heading and match scores.

import { type Source, SourceCards } from "@oration/canon/components/ai/source-cards";export function AssistPanel() {    const knowledge: Source[] = [        {            id: "kb-ach-trace",            kind: "doc",            title: "Tracing an ACH the supplier hasn't received",            snippet: "Share the 15-digit trace number and the settlement date.",            meta: "Payments playbook",            score: 0.92,        },        {            id: "kb-remit",            kind: "doc",            title: "Resending a remittance advice",            snippet:                "Remittances can be resent to any address on the supplier record.",            meta: "Remittances",            score: 0.87,        },        {            id: "kb-returns",            kind: "doc",            title: "When an ACH is returned",            snippet:                "Returned payments reissue on the next run once the bank details are confirmed.",            meta: "Payment runs",            score: 0.77,        },    ];    return (        <aside className="flex w-80 flex-col gap-4 rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex flex-col gap-0.5">                <h3 className="text-13 font-medium text-foreground">Assist</h3>                <p className="text-xs text-muted-foreground">                    Orchard Street, where is my payment                </p>            </div>            <SourceCards                sources={knowledge}                layout="list"                title="From the knowledge base"            />        </aside>    );}

States#

Rest
1Payments playbook90% matchApproval cutoffsInvoices approved by 5:00 PM CT the day before a run are included in it.
Linked
1Payments playbook90% matchApproval cutoffsInvoices approved by 5:00 PM CT the day before a run are included in it.
Hover
1Payments playbook90% matchApproval cutoffsInvoices approved by 5:00 PM CT the day before a run are included in it.
Focus visible
1Payments playbook90% matchApproval cutoffsInvoices approved by 5:00 PM CT the day before a run are included in it.
import { type Source, SourceCard } from "@oration/canon/components/ai/source-cards";export function StatesMatrix() {    const source: Source = {        id: "st",        kind: "doc",        title: "Approval cutoffs",        snippet:            "Invoices approved by 5:00 PM CT the day before a run are included in it.",        meta: "Payments playbook",        score: 0.9,    };    const cells = [        { label: "Rest", className: "" },        {            label: "Linked",            className: "bg-muted shadow-[inset_0_0_0_1px_var(--border-strong)]",        },        { label: "Hover", className: "bg-muted" },        { label: "Focus visible", className: "ring-3 ring-ring/40" },    ];    return (        <div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">            {cells.map((cell) => (                <div key={cell.label} className="flex min-w-0 flex-col gap-2">                    <span className="text-xs text-muted-foreground">                        {cell.label}                    </span>                    <SourceCard                        source={source}                        index={1}                        className={cell.className}                    />                </div>            ))}        </div>    );}
States
StateTreatment
RestWell Gray at 70%.
LinkedWhile its citation or the card itself is hovered or focused: Well Gray at 100% with a 1px Firm Hairline inset.
HoverCards with an href deepen to Well Gray at 100% over 150ms.
Focus visibleLinked cards get a 3px Focus Indigo ring at 40%.
Citation openThe marker turns ink on a 10% ink fill and a 288px preview opens above it after 250ms.
Missing sourceA Citation with no matching source renders as a plain [n] superscript, not a control.
OverflowThe row layout scrolls sideways with snap points and fades the clipped edge.

Behavior#

  • SourcesProvider shares the list and the active source. Citation reads sources[index − 1] from it; SourceCards mounts its own provider if there isn't one.
  • Hovering or focusing a citation or a card sets it active, which highlights its partner. Leaving clears it.
  • Citation previews are Base UI preview cards: they open 250ms after hover or focus, close 120ms after leaving and close on Escape.
  • A source with href renders as a link. url sources open in a new tab with rel="noreferrer" and say so to screen readers; other kinds open in place.
  • Row layout: 224px cards in a horizontal scroller with snap and a 2rem fade on the clipped edge. Grid: columns of at least 12rem. List: a stack with 6px gaps.
  • Titles clamp to one line and snippets to two; the preview shows up to four lines.

Do and don't#

  1. 1Payments playbookApproval cutoffsApproved by 5:00 PM CT the day before a run.
  2. 2ERP paymentPayment PMT-58213ACH, sent Sep 25.
  3. 3Call on Sep 24Call with Halcyon“Is that the early-pay discount?”
Do. Set meta to where the source came from, so cards are told apart at a glance: Payments playbook, ERP payment, Call on Sep 24.
  1. 1DocumentApproval cutoffsApproved by 5:00 PM CT the day before a run.
  2. 2DocumentPayment PMT-58213ACH, sent Sep 25.
  3. 3DocumentCall with Halcyon“Is that the early-pay discount?”
Don't. Leave every card on its kind label. Three cards that all say Document make people open each one.

Paid on September 25 with the 2% early-pay discount.

  1. 1ERP paymentPayment PMT-58213ACH, $20,212.50, sent Sep 25.
  2. 2Supplier recordHalcyon terms2/10 net 30.
Do. Put a citation marker after each claim, matching the card that supports it.

Paid on September 25 with the 2% early-pay discount.

  1. 1ERP paymentPayment PMT-58213ACH, $20,212.50, sent Sep 25.
  2. 2Supplier recordHalcyon terms2/10 net 30.
Don't. List sources under an answer with no markers in the text. Nobody can tell which card backs which claim.

Content#

  • title is the source's own name, as the source writes it: Tracing an ACH the supplier hasn't received, Payment PMT-58177.
  • snippet is the passage that supports the answer, quoted as written. For calls, quote the speaker in curly quotes.
  • meta names the origin in two or three words: Payments playbook, Supplier record, Call on Sep 24.
  • Keep the default heading Sources under answers. In a panel where the sources are the result, name them: From the knowledge base. Pass title={null} when a surrounding heading already says it.
  • Only show score where people compare retrieval quality, such as a knowledge base test.

Accessibility#

  • Citations are named Source 2: Payment PMT-58177, not just 2. Without an href they are buttons that only preview; with one they are links.
  • The citation's hit area extends 4px past the chip on every side.
  • The preview opens on keyboard focus as well as hover, and Escape closes it.
  • The card list is a <section> labelled by its heading (or Sources when title is null), with an <ol> so the numbers are announced.
  • The score is announced with the word match. External links announce (opens in a new tab).
  • Kind icons are decorative; the origin text beside them says what the source is.
Keyboard interactions
KeysAction
TabMoves to each citation, then to each linked card.
EnterFollows a citation or card that has an href.
EscCloses an open citation preview.

Design tokens#

Design tokens
TokenUsed for
--mutedCard fill at 70%, linked and hover at 100%; citation chip
--border-strongThe 1px inset on a linked card
--backgroundThe number chip on a card
shadow-borderHairline lift on the number chip
--foregroundCitation fill at 10% and text when active; titles
--muted-foregroundOrigin, score, snippet and the citation at rest
--ringFocus ring on linked cards
--radius-lg10px card corners; 4px on the chips
scroll-fade-xClipped-edge fade on the row layout

API reference#

SourceCards

A numbered list of sources. Also exported: the Source and SourceKind types.

Other props spread onto Nothing. Only the props below are read..

Props of SourceCards
PropTypeDefaultDescription
sourcesRequiredSource[]No default{ id, title, snippet, kind, href?, score?, meta? }. kind is "doc" | "url" | "record" | "transcript"; score is 0 to 1.
layout"row" | "grid" | "list""row"Row scrolls sideways under a message, grid fills a panel, list stacks.
titlestring | null"Sources"Heading with the count. null hides it.
classNamestringNo defaultMerged onto the section.

SourceCard

One card, for a single pinned source or a custom list.

Other props spread onto Nothing.

Props of SourceCard
PropTypeDefaultDescription
sourceRequiredSourceNo defaultThe source to show.
indexnumberNo defaultShows the number chip.
classNamestringNo defaultMerged onto the card.

Citation

An inline marker with a hover preview.

Other props spread onto Nothing.

Props of Citation
PropTypeDefaultDescription
indexRequirednumberNo default1-based position in the provider's sources.
sourceSourceNo defaultPass the source directly when there is no provider.
classNamestringNo defaultMerged onto the marker.

SourcesProvider

Shares sources and the active highlight between citations and cards.

Props of SourcesProvider
PropTypeDefaultDescription
sourcesRequiredSource[]No defaultThe list citations index into.
childrenRequiredReact.ReactNodeNo defaultText with citations, and the cards.

useSources

Hook. Returns { sources, activeId, setActiveId } from the nearest provider, or null.

No props of its own.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

SourceCard on its own isn't used in the product, and SourceCards directly only in the contact center assist panel and Copilot answers. Everything else goes through StreamingText.

A citation without an href is a <button> that does nothing when pressed; it only previews on hover and focus.

No product data uses the url kind yet.

useSources is exported but missing from the registry's export list.