# Citation List

> The sources behind an AI answer: numbered rows with domain, engines that cited each page, and your own pages marked.

- **Slug:** `citation-list`
- **Kind:** component · **Category:** Agent
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/design/components/citation-list
- **All files in one bundle:** https://viewership.ai/design/components/citation-list.bundle.txt
- **Structured JSON:** https://viewership.ai/design/components/citation-list.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/design/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/citation-list/`:

   - `CitationList.astro` (raw: https://viewership.ai/design/raw/citation-list/CitationList.astro.txt)

npm packages: none.

## Usage

```astro
---
import CitationList from "../components/viewership/citation-list/CitationList.astro";
---

<CitationList />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import CitationList from "./CitationList.astro";
---

<div class="stage">
  <p class="q">"Best way to waterproof a canvas backpack?"</p>
  <CitationList
    own="northwind.test"
    sources={[
      { title: "How to rewax a canvas pack in ten minutes", url: "https://northwind.test/journal/rewax", engines: ["ChatGPT", "Perplexity"] },
      { title: "r/hiking: does waxed canvas actually keep rain out?", url: "https://reddit.test/r/hiking/waxed", engines: ["ChatGPT", "Gemini"] },
      { title: "Waxed canvas care: a field guide", url: "https://trailnotes.test/canvas-care", engines: ["AI Overviews"] },
      { title: "Northwind repair program", url: "https://help.northwind.test/repairs", engines: ["Perplexity"] },
    ]}
  />
</div>

<style>
  .stage {
    max-width: 620px;
    margin-inline: auto;
    padding: 32px 20px;
  }
  .q {
    margin: 0 0 14px 4px;
    font-size: 0.9375rem;
    color: var(--vs-ink-2);
  }
</style>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `sources` | `{ title: string; url: string; engines?: string[] }[]` |  | Cited pages, in citation order. |
| `heading` | `string` | `"Sources"` | Label above the list. |
| `own` | `string` |  | Your domain. Matching rows and subdomains get a "Yours" pill and an accent monogram. |

## Source

### `src/components/viewership/citation-list/CitationList.astro`

```astro
---
/**
 * CitationList: the sources behind an AI answer. Numbered rows with a
 * monogram, page title, domain and which engines cited it. Mark your own
 * domain with `own` and those rows get an accent "Yours" pill, so a reader
 * can see at a glance where the brand shows up.
 *
 * @example
 * <CitationList own="northwind.test" sources={[{ title: "How to rewax a canvas pack", url: "https://northwind.test/journal/rewax", engines: ["ChatGPT", "Perplexity"] }]} />
 */

interface Source {
  title: string;
  url: string;
  /** Engines that cited this page, e.g. ["ChatGPT", "Perplexity"]. */
  engines?: string[];
}
interface Props {
  sources: Source[];
  heading?: string;
  /** Your domain. Matching rows (and subdomains) are marked "Yours". */
  own?: string;
  class?: string;
}

const { sources, heading = "Sources", own, class: className } = Astro.props;

const host = (url: string) => {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
};
const rows = sources.map((s) => {
  const domain = host(s.url);
  const mine = !!own && (domain === own || domain.endsWith(`.${own}`));
  return { ...s, domain, mine, letter: domain.charAt(0).toUpperCase() };
});
const ownCount = rows.filter((r) => r.mine).length;
---

<div class:list={["vs-citation-list", className]}>
  <div class="vs-citation-list__head">
    <p class="vs-citation-list__heading">{heading}</p>
    <p class="vs-citation-list__count">
      {sources.length} cited{own && <> · <span>{ownCount} yours</span></>}
    </p>
  </div>
  <ol class="vs-citation-list__rows">
    {
      rows.map((r, i) => (
        <li class:list={{ "is-own": r.mine }}>
          <a href={r.url} target="_blank" rel="noopener noreferrer">
            <span class="vs-citation-list__num">{i + 1}</span>
            <span class="vs-citation-list__mono" aria-hidden="true">{r.letter}</span>
            <span class="vs-citation-list__text">
              <span class="vs-citation-list__title">{r.title}</span>
              <span class="vs-citation-list__domain">
                {r.domain}
                {r.mine && <span class="vs-citation-list__yours">Yours</span>}
              </span>
            </span>
            {
              r.engines && r.engines.length > 0 && (
                <span class="vs-citation-list__engines">
                  <span class="vs-sr-only">Cited by</span>
                  {r.engines.map((e) => (
                    <span class="vs-citation-list__engine">{e}</span>
                  ))}
                </span>
              )
            }
          </a>
        </li>
      ))
    }
  </ol>
</div>

<style>
  .vs-citation-list {
    container-type: inline-size;
    padding: 6px;
    border-radius: calc(var(--vs-radius-window) + 4px);
    background: var(--vs-surface);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    font-stretch: var(--vs-font-stretch);
    box-shadow: var(--vs-shadow-card);
  }
  .vs-citation-list__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px 8px;
  }
  .vs-citation-list__heading {
    margin: 0;
    font-size: 0.9375rem;
    font-weight: 600;
  }
  .vs-citation-list__count {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--vs-ink-3);
  }
  .vs-citation-list__count span {
    color: var(--vs-accent-ink);
  }
  .vs-citation-list__rows {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .vs-citation-list__rows a {
    display: grid;
    grid-template-columns: 18px 32px minmax(0, 1fr);
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: var(--vs-radius-card);
    color: inherit;
    text-decoration: none;
    transition: background-color var(--vs-duration-fast) ease-out;
    animation: vs-fade-up var(--vs-duration) var(--vs-ease-out) both;
  }
  .vs-citation-list__rows li:nth-child(2) a { animation-delay: 40ms; }
  .vs-citation-list__rows li:nth-child(3) a { animation-delay: 80ms; }
  .vs-citation-list__rows li:nth-child(4) a { animation-delay: 120ms; }
  .vs-citation-list__rows li:nth-child(n + 5) a { animation-delay: 160ms; }
  .vs-citation-list__rows a:hover {
    background: var(--vs-hover);
  }
  .vs-citation-list__rows a:focus-visible {
    outline: none;
    box-shadow: var(--vs-focus-ring);
  }
  .vs-citation-list__num {
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
    color: var(--vs-ink-3);
    text-align: right;
  }
  .vs-citation-list__mono {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: var(--vs-radius-control);
    background: var(--vs-inset);
    border: 1px solid var(--vs-line);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--vs-ink-2);
  }
  .is-own .vs-citation-list__mono {
    background: var(--vs-accent-tint);
    border-color: transparent;
    color: var(--vs-accent-ink);
  }
  .vs-citation-list__text {
    display: grid;
    gap: 2px;
    min-width: 0;
  }
  .vs-citation-list__title {
    overflow: hidden;
    font-size: 0.9375rem;
    font-weight: 500;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .vs-citation-list__domain {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.8125rem;
    color: var(--vs-ink-3);
  }
  .vs-citation-list__yours {
    padding: 1px 8px;
    border-radius: var(--vs-radius-pill);
    background: var(--vs-accent-tint);
    color: var(--vs-accent-ink);
    font-size: 0.75rem;
    font-weight: 500;
  }
  .vs-citation-list__engines {
    display: none;
    gap: 4px;
    justify-content: flex-end;
  }
  .vs-citation-list__engine {
    padding: 3px 8px;
    border-radius: var(--vs-radius-pill);
    background: var(--vs-inset);
    border: 1px solid var(--vs-line-soft);
    font-size: 0.75rem;
    color: var(--vs-ink-2);
    white-space: nowrap;
  }
  @container (min-width: 520px) {
    .vs-citation-list__rows a {
      grid-template-columns: 18px 32px minmax(0, 1fr) auto;
    }
    .vs-citation-list__engines {
      display: flex;
    }
  }
</style>
```
