Sources and context

Where an answer came from, and what it cost. Sources is the collapsed list under a turn, InlineCitation is the marker inside the prose, and Context is the token budget.

Import

import {
  Source,
  Sources,
  SourcesContent,
  SourcesTrigger,
} from '@elirobinson/ai-elements/components/sources';
import {
  InlineCitation,
  InlineCitationCard,
  InlineCitationCardBody,
  InlineCitationCardTrigger,
  InlineCitationText,
} from '@elirobinson/ai-elements/components/inline-citation';
import {
  Context,
  ContextContent,
  ContextTrigger,
} from '@elirobinson/ai-elements/components/context';

Source renders an <a href target="_blank" rel="noreferrer">. Three things follow:

  • rel is noreferrer, and that is the whole of it: the HTML spec defines noreferrer as implying noopener, and a target="_blank" link is implicitly noopener in current browsers regardless. There is nothing to add. {...props} is spread last, so the only reason to override it is wanting the referrer sent — <Source rel="noopener" …>.
  • The visible text is the title you pass, not the host of the href. Nothing derives the label from the URL. This is the one that matters, because these URLs came from a model which got them from retrieved content: a link can say one thing and go somewhere else unless you build the label out of the URL yourself.
  • SourcesTrigger reads "Used N sources" from its count prop. The count is a number you give it; it is not derived from the children.

Inline citations

Target Size (Minimum) is 24 by 24.example.com

Closed

This demo and the one below it are shown closed. Both previews are hover cards, which render into a portal on document.body — open by default they would sit on top of the page rather than inside the box — so opening one is the reader's move.

InlineCitationCardTrigger is not a number. It renders new URL(sources[0]).hostname, plus +N when there is more than one — so the marker in the prose is the host, and it is derived from the URL rather than supplied beside it.

Two consequences worth knowing before you wire it to model output:

  • new URL() throws on anything that is not an absolute URL. A relative or malformed href from a model takes the render down with it; validate before you pass it.
  • The trigger is a Badge, which renders a <span>. It has no tabindex and no role, so it is not in the tab order and the hover card is a pointer-only affordance. Pass your own focusable element as the trigger if the preview has to be reachable.

InlineCitationCarouselPrev and …Next are real buttons with aria-labels of "Previous" and "Next", both floored to var(--target) — see below.

Context

Closed

ContextTrigger's default child is a Button, so unlike the citation marker this hover card does open on focus as well as on hover. The percentage it shows is usedTokens / maxTokens formatted with a hard-coded en-US locale; pass your own child to format it yourself.

What this system changed

PatchWhat changed, and why
button-floorprimary — ui/button.tsx — every <Button> in the tree. Measured default 36px, sm 32px, icon 36x36, icon-sm 32x32, icon-lg 40x40. The floor every vendored button gets unless it is classified dense elsewhere in this list. shadcn sizes at 32-40px, which clears AA and misses this system’s AAA default; the `not-data-[touch-target=dense]` guard is what keeps that a default rather than a blanket, and it cannot be undone by a consumer’s className the way a merged utility could.
sources-trigger-floorprimary — sources.tsx — SourcesTrigger. Measured 110x16. A 16px-tall disclosure row misses the dense floor too, so there is no second tier to fall back to.
source-link-floorprimary — sources.tsx — Source (the citation link). Measured 152x16. A citation marker: an a[href] that navigates away from the page, sitting in a list where every row is free to be taller.
inline-citation-carousel-paging-floorprimary — inline-citation.tsx — InlineCitationCarouselPrev and …Next. Measured 16x16 each. Carousel paging is pagination, which contracts.json names as primary in the same breath as buttons and nav items.

Read from vendoredElementTargets in @elirobinson/ai-patterns/contracts, which the browser audit writes.