Source cards
Retrieved context as cards, with inline citation markers that point at them.
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
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
SourceCardwhen one piece of context is pinned beside a draft. - Through
StreamingText'scitationsprop, 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
citationsand 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
The Tabular Figures Rule
Anatomy#
The payment went out on Friday.
- 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.
- Number. The card's position, in a white chip with a hairline lift, matching the citation.
- Kind icon. 14px: a document, a globe for web pages, a database for records, audio lines for call transcripts.
- Origin.
metain 12px Slate Meta, such as Payments playbook or Call on Sep 24. Falls back to the kind's name. - Score. Optional retrieval match at the right, 92%, announced as 92% match.
- Title. 13px medium, one line.
- 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
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
- 1Knowledge baseRemittance advice FAQDiscounts appear as a separate line on the remittance advice, not on the payment itself.
- 2Web pageACH returns and reason codesR01 means insufficient funds; R03 means no account or unable to locate the account.(opens in a new tab)
- 3Supplier recordHalcyon termsPayment terms 2/10 net 30. Early payment enrolled March 2026.
- 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].
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
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#
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> );}| State | Treatment |
|---|---|
| Rest | Well Gray at 70%. |
| Linked | While its citation or the card itself is hovered or focused: Well Gray at 100% with a 1px Firm Hairline inset. |
| Hover | Cards with an href deepen to Well Gray at 100% over 150ms. |
| Focus visible | Linked cards get a 3px Focus Indigo ring at 40%. |
| Citation open | The marker turns ink on a 10% ink fill and a 288px preview opens above it after 250ms. |
| Missing source | A Citation with no matching source renders as a plain [n] superscript, not a control. |
| Overflow | The row layout scrolls sideways with snap points and fades the clipped edge. |
Behavior#
SourcesProvidershares the list and the active source.Citationreadssources[index − 1]from it;SourceCardsmounts 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
hrefrenders as a link.urlsources open in a new tab withrel="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#
- 1Payments playbookApproval cutoffsApproved by 5:00 PM CT the day before a run.
- 2ERP paymentPayment PMT-58213ACH, sent Sep 25.
- 3Call on Sep 24Call with Halcyon“Is that the early-pay discount?”
meta to where the source came from, so cards are told apart at a glance: Payments playbook, ERP payment, Call on Sep 24.- 1DocumentApproval cutoffsApproved by 5:00 PM CT the day before a run.
- 2DocumentPayment PMT-58213ACH, sent Sep 25.
- 3DocumentCall with Halcyon“Is that the early-pay discount?”
Paid on September 25 with the 2% early-pay discount.
- 1ERP paymentPayment PMT-58213ACH, $20,212.50, sent Sep 25.
- 2Supplier recordHalcyon terms2/10 net 30.
Paid on September 25 with the 2% early-pay discount.
- 1ERP paymentPayment PMT-58213ACH, $20,212.50, sent Sep 25.
- 2Supplier recordHalcyon terms2/10 net 30.
Content#
titleis the source's own name, as the source writes it: Tracing an ACH the supplier hasn't received, Payment PMT-58177.snippetis the passage that supports the answer, quoted as written. For calls, quote the speaker in curly quotes.metanames 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
scorewhere people compare retrieval quality, such as a knowledge base test.
Accessibility#
- Citations are named Source 2: Payment PMT-58177, not just 2. Without an
hrefthey 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 whentitleis 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.
| Keys | Action |
|---|---|
| Tab | Moves to each citation, then to each linked card. |
| Enter | Follows a citation or card that has an href. |
| Esc | Closes an open citation preview. |
Design tokens#
| Token | Used for |
|---|---|
--muted | Card fill at 70%, linked and hover at 100%; citation chip |
--border-strong | The 1px inset on a linked card |
--background | The number chip on a card |
shadow-border | Hairline lift on the number chip |
--foreground | Citation fill at 10% and text when active; titles |
--muted-foreground | Origin, score, snippet and the citation at rest |
--ring | Focus ring on linked cards |
--radius-lg | 10px card corners; 4px on the chips |
scroll-fade-x | Clipped-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..
| Prop | Type | Default | Description |
|---|---|---|---|
sourcesRequired | Source[] | 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. |
title | string | null | "Sources" | Heading with the count. null hides it. |
className | string | No default | Merged onto the section. |
SourceCard
One card, for a single pinned source or a custom list.
Other props spread onto Nothing.
| Prop | Type | Default | Description |
|---|---|---|---|
sourceRequired | Source | No default | The source to show. |
index | number | No default | Shows the number chip. |
className | string | No default | Merged onto the card. |
Citation
An inline marker with a hover preview.
Other props spread onto Nothing.
| Prop | Type | Default | Description |
|---|---|---|---|
indexRequired | number | No default | 1-based position in the provider's sources. |
source | Source | No default | Pass the source directly when there is no provider. |
className | string | No default | Merged onto the marker. |
SourcesProvider
Shares sources and the active highlight between citations and cards.
| Prop | Type | Default | Description |
|---|---|---|---|
sourcesRequired | Source[] | No default | The list citations index into. |
childrenRequired | React.ReactNode | No default | Text 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.