# Render citation highlights in your own viewer

**Outcome:** clicking a citation scrolls your PDF viewer to the exact sentence and draws a highlight
over it.

## The locator contract

Each Citation location gives `page` (1-indexed) and a `bbox` in fractions of page width
and height, origin top-left.

```
left   = bbox.x      * renderedWidth
top    = bbox.y      * renderedHeight
width  = bbox.width  * renderedWidth
height = bbox.height * renderedHeight
```

Because the values are fractions, the same numbers work at any zoom and any DPI.

## Walkthrough

Take one real citation — the encoder-layers claim from the Transformer paper, page 7:

```json
{
  "index": 1,
  "quote": "The encoder is composed of a stack of N = 6 identical layers.",
  "source": { "type": "document", "id": "doc_8ba2f01c47" },
  "locations": [
    { "page": 7, "bbox": { "x": 0.252, "y": 0.426, "width": 0.495, "height": 0.041 } }
  ]
}
```

`bbox` values are **fractions of the page, origin top-left** — not pixels, not PDF points. That is
deliberate: they stay correct at any zoom, and you never need to know the source page's dimensions.

## Positioning the overlay

Multiply each fraction by the rendered page box. Your page element must be `position: relative`.

```typescript
type BBox = { x: number; y: number; width: number; height: number };

function highlight(pageEl: HTMLElement, bbox: BBox, index: number) {
  const { width: pw, height: ph } = pageEl.getBoundingClientRect();

  const el = document.createElement("mark");
  el.dataset.citationIndex = String(index);
  Object.assign(el.style, {
    position: "absolute",
    left: `${bbox.x * pw}px`,
    top: `${bbox.y * ph}px`,
    width: `${bbox.width * pw}px`,
    height: `${bbox.height * ph}px`,
    background: "rgba(234, 88, 12, 0.24)",
    borderRadius: "2px",
    pointerEvents: "none",
  });

  pageEl.appendChild(el);
  return el;
}
```

For the citation above on a page rendered 816 × 1056, that lands the box at `left: 205.6px`,
`top: 449.9px`, `width: 403.9px`, `height: 43.3px`.

## Scrolling to it

Render first, then scroll — a zero-size element scrolls to the wrong place.

```typescript
function revealCitation(pageEl: HTMLElement, bbox: BBox, index: number) {
  const el = highlight(pageEl, bbox, index);
  requestAnimationFrame(() => {
    el.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
  });
}
```

## Two things that will bite you

**Re-measure on resize.** The fractions do not change; the pixels do. Recompute on
`ResizeObserver`, not on `window.resize` alone.

**A citation can carry several locations.** A claim spanning a page break returns one entry per
page. Render all of them and scroll to the first:

```typescript
citation.locations.forEach((loc, i) => {
  const pageEl = pages[loc.page - 1];   // page is 1-indexed
  if (i === 0) revealCitation(pageEl, loc.bbox, citation.index);
  else highlight(pageEl, loc.bbox, citation.index);
});
```

> [!warning] `page` is 1-indexed
> Your page array almost certainly is not. Off-by-one here shows the reader a confidently wrong
> passage, which is worse than showing none.

## Handling the hard parts

- **Rotated pages.** Check page rotation before mapping coordinates.
- **Multi-box quotes.** `locations` can hold several boxes for one quote — draw all of them.
- **Missing locations.** Corpus papers whose PDF we do not hold return a quote but no boxes. Fall back to a text search within the page, then to the paper page.

## Related

citation · grounded-answers · documents
