Sources list
The full set of sources behind an answer: behind one button, as cards or a scrolling row, as numbered footnotes, in a side panel, or as the log of what a research run read and used.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/sources-list-stack.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/sources-list-stack.json
Add citations and source cards to an AI answer →
Usage & API
Example
import { SourcesStack } from "@/components/ai/sources-list-stack"
<SourcesStack
sources={[]}
open={true}
onOpenChange={() => {}}
/>Props SourcesStack
| Prop | Type & description | Required |
|---|---|---|
sources | Source[] | Required |
open | boolean | Required |
onOpenChange | (open: boolean) => void | Required |
preview | numberHow many favicons to show in the closed button. | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Favicon stack exports SourcesStack with sources, open, onOpenChange, and preview, default 4. A source has title, domain, color, and an optional url. The closed control is a pill: overlapping letter marks, the count as {n} sources, and a chevron. Opening it shows a numbered list, at most 232px tall, with the title and, from the @md container size, the domain. If url is set the row is a link. open is controlled. The component does not remember it. The preview data is a four-day-week reading list, starting with autonomy.work, bbc.co.uk, and theguardian.com.
Example
import { SourceCards } from "@/components/ai/sources-list-cards"
<SourceCards
sources={[]}
/>Props SourceCards
| Prop | Type & description | Required |
|---|---|---|
sources | Source[] | Required |
limit | numberCards shown before the "more" card. | Optional |
Dependencies
- React
- Tailwind CSS
- motion
Implementation notes
Cards exports SourceCards with sources and limit, default 3. The source shape matches the stack. Each card is at least 88px tall, with a two-line title, a letter mark, the domain, and the 1-based index. A source with url is an anchor. Past limit, one more card shows three marks and +n more, and the same button reads Fewer once everything is open. That toggle is internal state, unlike the stack's open prop. The preview asks why a sourdough starter stopped rising and lists seven pages, from kingarthurbaking.com through ncbi.nlm.nih.gov.
Example
import { SourceCarousel } from "@/components/ai/sources-list-carousel"
<SourceCarousel
sources={[]}
active={null}
onActiveChange={() => {}}
expanded={true}
onExpandedChange={() => {}}
/>Props SourceCarousel
| Prop | Type & description | Required |
|---|---|---|
sources | CarouselSource[] | Required |
visible | numberCards in the row before the "+n more" tile. | Optional |
active | number | null1-based number of the source being pointed at, from a card or a marker. | Required |
onActiveChange | (n: number | null) => void | Required |
expanded | boolean | Required |
onExpandedChange | (expanded: boolean) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Carousel exports SourceCarousel with sources, visible, default 6, active, onActiveChange, expanded, and onExpandedChange. A CarouselSource has title, site, color, and an optional url. Cards are 158px wide and 78px tall in a snap row. The active card, matched by a 1-based number, uses an indigo border. Pointer and focus call onActiveChange. Previous and Next buttons appear only when the row can scroll that way, and they scroll by about one viewport. The leftover tile reads +n more or Show less and calls onExpandedChange. Edge fades use a mask. The expanded grid of the rest of the sources is wired through expanded.
Example
import { FootnotedAnswer } from "@/components/ai/sources-list-footnotes"
<FootnotedAnswer
title="…"
blocks={[]}
references={[]}
/>Props FootnotedAnswer
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
blocks | Block[] | Required |
references | Reference[] | Required |
onCopy | (format: CitationFormat, text: string) => void | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Footnotes exports FootnotedAnswer with title, blocks, references, and onCopy. A block is a heading or a paragraph of runs. A run is a string or { ref }, and ref is 1-based. A reference has authors, title, publication, date, an optional url, and ready-made cite strings for apa and mla. Clicking a superscript scrolls the article so the reference sits about a third of the way down and flashes it. Each reference has a back link, labelled Back to citation, or Back to citation a, b, and so on when the same work is cited more than once. Copy citations opens a menu, APA 7th edition or MLA 9th edition, copies every reference's matching string, and calls onCopy with the format and the text. The article itself caps at 392px and scrolls.
Example
import { SourcesPanel } from "@/components/ai/sources-list-panel"
<SourcesPanel
open={true}
onClose={() => {}}
sources={[]}
sentences={[]}
tab="cited"
onTabChange={() => {}}
expanded={null}
onExpandedChange={() => {}}
/>Props SourcesPanel
| Prop | Type & description | Required |
|---|---|---|
open | boolean | Required |
onClose | () => void | Required |
sources | PanelSource[] | Required |
sentences | AnswerSentence[]Used to show which sentences cite each source. Numbers are 1-based indexes into `sources`. | Required |
tab | "cited" | "read" | Required |
onTabChange | (tab: "cited" | "read") => void | Required |
expanded | number | nullIndex of a row to open, e.g. after clicking a marker. | Required |
onExpandedChange | (index: number | null) => void | Required |
autoFocus | booleanMove focus into the sheet when it opens. Turn off for an automatic open. | Optional |
mode | "overlay" | "docked""overlay" floats over the page with a scrim; "docked" sits beside it and pushes it narrower. | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Side panel exports SourcesPanel with open, onClose, sources, sentences, tab, onTabChange, expanded, onExpandedChange, autoFocus, default true, and mode, default overlay. A PanelSource has title, site, date, snippet, color, an optional url, and cited. tab is cited or read. A search field filters title, site, and snippet, and focuses when the panel opens unless autoFocus is false. Escape calls onClose. Tab cycles inside the panel. overlay draws a floating sheet. docked sits beside the page at 388px. Expanding a row is controlled, so a citation marker in the answer can set expanded to that index. sentences are { text, cites } with 1-based indexes, used to show which sentences mention a source.
Example
import { ResearchLog } from "@/components/ai/sources-list-research"
<ResearchLog
groups={[]}
duration="…"
filter={filter}
onFilterChange={() => {}}
open={[]}
onOpenChange={() => {}}
/>Props ResearchLog
| Prop | Type & description | Required |
|---|---|---|
groups | QueryGroup[] | Required |
duration | stringRun time, already formatted, e.g. "4m 12s". | Required |
filter | ResearchFilter | Required |
onFilterChange | (filter: ResearchFilter) => void | Required |
open | string[]Queries whose groups are open. | Required |
onOpenChange | (open: string[]) => void | Required |
maxHeight | numberTallest the list grows before it scrolls. | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Research log exports ResearchLog with groups, duration, filter, onFilterChange, open, onOpenChange, and maxHeight, default 264. A group is a query plus sources. A source is cited with a ref number, read, or skipped with a reason of paywalled, duplicate, or older than 2023. The header reads Read {n} sources, the cited count, and duration, which you format, for example 4m 12s. A radio group switches All, Cited, and Skipped, and each option shows its count. Groups that have no row for the current filter disappear. open is the list of query strings that are expanded. The list grows with its content until maxHeight, then scrolls.
Choosing a layout
Use the stack when the answer is short and the sources are a disclosure: one button, then the titles. Use cards when a handful of pages should be visible without a click, with the rest behind +n more. Use the carousel when the row is wide and a marker in the answer should light the matching card. Use footnotes when the answer is a document with a reference list and someone may need to copy it. Use the side panel when the thread stays on screen and the sources open beside it. Use the research log when the reader cares which query found a page, and which pages were paywalled, duplicated, or too old.
Do not use this list as the only citation if the claim itself needs a marker. A pill, a superscript, a domain chip, or a quoted passage is Inline citation, and it can sit in the same answer as a list. Do not use the research log as a live chain of thought. Chain of thought's research trail shows searches and notes as they arrive, one action at a time. The log is the record after the run, with a duration string and filters. A single pasted URL, with a skeleton while its title loads, is Link preview.
Which of these keep their open state for me?
SourcesStack, SourceCarousel, SourcesPanel, and ResearchLog are controlled. You pass open, expanded, tab, or filter, and the matching change callback. SourceCards keeps the more/fewer toggle itself, and there is no prop for it. FootnotedAnswer keeps the copy menu and the flash itself. If a marker in the answer should open a particular source, use the panel's expanded index or the carousel's active number, both of which you set from outside.
What does a skipped page look like in the research log?
Set status to skipped and reason to paywalled, duplicate, or older than 2023. The row shows that reason, with a lock, a copy-off mark, or a clock. A cited row shows Cited and the ref number. A read row shows Read. The Skipped filter hides the other two, and a query group with no remaining rows is removed from the list. The header count always includes every source, because it is computed before the filter. duration is not calculated from those statuses. You pass the string.